The bundles carried all thirteen translations at once and a page fetched them a third time as raw source; each page now loads only its own language. Alongside that: JavaScript is minified, the eight stylesheets are served as one file, the QR scanner is fetched after the app is up instead of on every visit, Inter ships once rather than five copies of the same file, and Font Awesome is subset to the 82 icons this app draws instead of all 2468. 1.85 MB across 43 requests becomes under 700 KB across 33. On mobile the page starts drawing in 1.6 s instead of 6.3 s and is usable in 4.5 s instead of 11 s. Pages also carry their text in the HTML now. Everything was drawn by JavaScript into an empty div, so crawlers saw correct metadata around nothing, and twelve of the thirteen language pages had never been shown to anyone. The documentation is published under /docs/ with a new FAQ, and unknown addresses return a real 404. Separately: the localized shells were served with the year-long immutable cache header meant for static assets, which pinned anyone who opened /de/ or /ru/ to that build. The header is fixed and the service worker refreshes what it cached. Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2
284 lines
16 KiB
HTML
284 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
|
change the Markdown instead. -->
|
|
<html lang="en" dir="ltr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
|
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
|
<title>Configuration - SecureBit.chat</title>
|
|
<meta name="description" content="npm run build compiles the CSS, bundles the JavaScript into dist/, and regenerates meta.json with a build stamp. That stamp is what the update mechanism…">
|
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
|
<link rel="canonical" href="https://securebit.chat/docs/configuration/">
|
|
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
|
<meta property="og:site_name" content="SecureBit.chat">
|
|
<meta property="og:title" content="Configuration - SecureBit.chat">
|
|
<meta property="og:description" content="npm run build compiles the CSS, bundles the JavaScript into dist/, and regenerates meta.json with a build stamp. That stamp is what the update mechanism…">
|
|
<meta property="og:url" content="https://securebit.chat/docs/configuration/">
|
|
<meta property="og:type" content="article">
|
|
<meta property="og:locale" content="en_US">
|
|
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="Configuration - SecureBit.chat">
|
|
<meta name="twitter:description" content="npm run build compiles the CSS, bundles the JavaScript into dist/, and regenerates meta.json with a build stamp. That stamp is what the update mechanism…">
|
|
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "TechArticle",
|
|
"headline": "Configuration",
|
|
"description": "npm run build compiles the CSS, bundles the JavaScript into dist/, and regenerates meta.json with a build stamp. That stamp is what the update mechanism…",
|
|
"url": "https://securebit.chat/docs/configuration/",
|
|
"inLanguage": "en",
|
|
"isPartOf": {
|
|
"@type": "WebSite",
|
|
"@id": "https://securebit.chat/#website"
|
|
},
|
|
"publisher": {
|
|
"@id": "https://securebit.chat/#organization"
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
:root { color-scheme: dark; }
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: #0f0f11;
|
|
color: #d6d6dc;
|
|
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
font-size: 16px;
|
|
line-height: 1.68;
|
|
}
|
|
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
|
.top {
|
|
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
|
padding-bottom: 16px; margin-bottom: 40px;
|
|
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
font-size: 13.5px;
|
|
}
|
|
.top a { color: #8a8a92; text-decoration: none; }
|
|
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
|
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
|
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
|
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
|
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
|
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
|
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
|
p, li { color: #a9a9b3; }
|
|
p { margin: 0 0 16px; }
|
|
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
|
li { margin: 5px 0; }
|
|
a { color: #f0892a; text-underline-offset: 2px; }
|
|
strong { color: #e8e8eb; }
|
|
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
|
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
|
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
|
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
|
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
|
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
|
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
|
td:first-child, th:first-child { padding-inline-start: 0; }
|
|
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
|
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
|
img { max-width: 100%; height: auto; }
|
|
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
|
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
|
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
|
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
|
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
|
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<nav class="top">
|
|
<a class="brand" href="/">SecureBit.chat</a>
|
|
<a href="/docs/">Documentation</a>
|
|
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
|
<span class="here">Configuration</span>
|
|
</nav>
|
|
<h1>Configuration</h1>
|
|
<h2 id="requirements">Requirements</h2>
|
|
<ul>
|
|
<li>A browser with WebRTC and Web Crypto support</li>
|
|
<li>Node.js 18 or later, for building</li>
|
|
<li>A TURN service, if you need relay-only privacy mode or you expect users behind
|
|
strict NAT</li>
|
|
</ul>
|
|
<h2 id="building-and-running">Building and running</h2>
|
|
<pre><code class="language-bash">npm install
|
|
npm run build
|
|
npm run serve
|
|
</code></pre>
|
|
<p><code>npm run build</code> compiles the CSS, bundles the JavaScript into <code>dist/</code>, and
|
|
regenerates <code>meta.json</code> with a build stamp. That stamp is what the update
|
|
mechanism compares, and it is also written into <code>sw.js</code> so the browser reinstalls
|
|
the service worker on each release. A deployment that skips <code>post-build</code> will not
|
|
notify anyone that an update exists.</p>
|
|
<p>The application is static. Any web server can host it, and there is no backend to
|
|
run.</p>
|
|
<h2 id="ice-and-turn">ICE and TURN</h2>
|
|
<p>WebRTC needs to discover network paths between the two browsers. STUN is enough
|
|
to learn a public address; TURN is needed when a direct path cannot be
|
|
established, and is the only way to keep peers from seeing each other's IP
|
|
addresses.</p>
|
|
<p>Configuration comes from three places, in order of precedence:</p>
|
|
<ol>
|
|
<li>User settings under Advanced network settings, stored in IndexedDB per device</li>
|
|
<li><code>config/ice-servers.js</code>, an operator override loaded before the application</li>
|
|
<li>Built-in public STUN defaults</li>
|
|
</ol>
|
|
<p><code>config/ice-servers.js</code> is not committed, because it is where operator TURN
|
|
credentials would go. Use <code>config/ice-servers.example.js</code> as the template. The
|
|
Docker image copies <code>config/ice-servers.prod.js</code> into place at build time.</p>
|
|
<pre><code class="language-js">window.SECUREBIT_ICE_SERVERS = [
|
|
{ urls: 'stun:stun.example.org:3478' },
|
|
{
|
|
urls: [
|
|
'turn:turn.example.org:3478?transport=udp',
|
|
'turn:turn.example.org:3478?transport=tcp',
|
|
'turns:turn.example.org:443?transport=tcp'
|
|
],
|
|
username: '...',
|
|
credential: '...'
|
|
}
|
|
];
|
|
</code></pre>
|
|
<p>Offering several transports is worth the extra lines. UDP is the most widely
|
|
usable, TCP covers networks that block UDP, and TURNS on 443 gets through
|
|
firewalls that only allow HTTPS. Some browsers also fail to resolve STUN and TURN
|
|
hostnames inside their WebRTC layer even when ordinary page DNS works, so listing
|
|
a raw IP alongside the hostname is a reasonable fallback.</p>
|
|
<p>Any TURN credential shipped to a browser is public by definition, because the
|
|
browser has to be able to read it. Treat it as a shared resource and apply quotas
|
|
on the TURN server rather than relying on the credential staying secret.</p>
|
|
<h3 id="user-supplied-servers">User-supplied servers</h3>
|
|
<p>Users can paste their own STUN and TURN servers. Input is validated against an
|
|
allowlist before it reaches <code>RTCPeerConnection</code>: only the <code>stun</code>, <code>stuns</code>, <code>turn</code>
|
|
and <code>turns</code> schemes, a hostname or bracketed IPv6 address with an optional port,
|
|
and an optional <code>transport=udp</code> or <code>transport=tcp</code> query. At most 10 servers with
|
|
8 URLs each. Anything else is rejected with a specific reason.</p>
|
|
<h3 id="privacy-modes">Privacy modes</h3>
|
|
<div class="tablewrap"><table>
|
|
<thead>
|
|
<tr>
|
|
<th>Mode</th>
|
|
<th>Behaviour</th>
|
|
<th>IP exposure</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody><tr>
|
|
<td>Default</td>
|
|
<td>Standard candidate gathering</td>
|
|
<td>Direct candidates can reveal addresses to the peer</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Relay-only</td>
|
|
<td>Sets <code>iceTransportPolicy: "relay"</code></td>
|
|
<td>Requires TURN; peers see only the relay</td>
|
|
</tr>
|
|
</tbody></table></div>
|
|
<p>STUN is not a substitute for TURN here. It reveals your public address to the
|
|
peer by design. Relay-only mode without a configured TURN server cannot connect
|
|
at all, and the interface warns when TURN is missing.</p>
|
|
<p>Validate a TURN deployment with <code>chrome://webrtc-internals</code> before relying on it.
|
|
A relay candidate should appear in the gathered set; if none does, the credentials
|
|
or the ports are wrong.</p>
|
|
<h3 id="when-connections-fail">When connections fail</h3>
|
|
<p>Candidate gathering finishes only when every configured server has replied or
|
|
timed out. Behind a VPN or a restrictive firewall that may never happen, and the
|
|
console will show <code>701</code> errors for each unreachable server.</p>
|
|
<p>The application handles this: it proceeds as soon as there are usable candidates
|
|
and only keeps waiting while there are none, up to 25 seconds. Host candidates
|
|
alone are often enough on a local network. If nothing at all is gathered, the
|
|
message names the likely causes, which in practice are a VPN binding the browser
|
|
to an interface that cannot reach the servers, or a firewall dropping UDP.</p>
|
|
<h2 id="file-transfer-policy">File transfer policy</h2>
|
|
<p>Incoming transfers are validated before the consent prompt and require explicit
|
|
approval.</p>
|
|
<div class="tablewrap"><table>
|
|
<thead>
|
|
<tr>
|
|
<th>Category</th>
|
|
<th>Extensions</th>
|
|
<th>Size limit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody><tr>
|
|
<td>Images</td>
|
|
<td><code>.jpg</code> <code>.jpeg</code> <code>.png</code> <code>.gif</code> <code>.webp</code> <code>.bmp</code> <code>.ico</code></td>
|
|
<td>25 MB</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Documents</td>
|
|
<td><code>.pdf</code></td>
|
|
<td>50 MB</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Text</td>
|
|
<td><code>.txt</code></td>
|
|
<td>10 MB</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Archives</td>
|
|
<td><code>.zip</code></td>
|
|
<td>100 MB</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Voice</td>
|
|
<td><code>.webm</code> <code>.ogg</code> <code>.oga</code> <code>.opus</code> <code>.m4a</code> <code>.mp4</code> <code>.mp3</code> <code>.wav</code></td>
|
|
<td>20 MB</td>
|
|
</tr>
|
|
</tbody></table></div>
|
|
<p>Overall ceiling is 100 MB per file.</p>
|
|
<p>Blocked outright: <code>.exe</code> <code>.bat</code> <code>.cmd</code> <code>.sh</code> <code>.js</code> <code>.msi</code> <code>.dmg</code> <code>.app</code> <code>.jar</code>
|
|
<code>.scr</code> <code>.ps1</code> <code>.vbs</code> <code>.html</code> <code>.svg</code></p>
|
|
<p>The extension list is the security boundary. MIME type is treated as advisory,
|
|
because it is client-supplied, varies between browsers and operating systems, and
|
|
is frequently absent. An allowed extension is accepted when the MIME type is
|
|
absent, generic, or one of the recognised types, but a clearly contradictory MIME
|
|
type is rejected as a spoofing signal.</p>
|
|
<p>Voice notes are the one transfer accepted without a prompt, so they are checked
|
|
more strictly. The receiver decides, not the sender: a genuine audio MIME type,
|
|
at most 4 MB, and a 64 MB budget for the whole session. A transfer that fails
|
|
those checks is not rejected, it simply loses the shortcut and appears as a normal
|
|
file with the usual prompt.</p>
|
|
<h2 id="deployment-notes">Deployment notes</h2>
|
|
<p>The repository includes an nginx configuration (<code>deploy/nginx.conf</code>) and an
|
|
Apache one (<code>.htaccess</code>). Both set the same policy, and the important parts are:</p>
|
|
<ul>
|
|
<li><code>index.html</code>, <code>sw.js</code>, <code>manifest.json</code>, <code>meta.json</code> and <code>config/ice-servers.js</code>
|
|
must not be cached. A stale <code>meta.json</code> breaks update notification, and a stale
|
|
<code>sw.js</code> freezes the service worker.</li>
|
|
<li><code>dist/</code> bundles are query-versioned, so <code>no-cache</code> with revalidation is enough
|
|
and avoids re-downloading unchanged bundles.</li>
|
|
<li><code>CDN-Cache-Control</code> is set separately, because a CDN reads it independently of
|
|
the browser directive and will otherwise happily serve a stale app shell.</li>
|
|
<li><code>frame-ancestors</code> and HSTS have to be sent as headers. The rest of the content
|
|
security policy is a meta tag in <code>index.html</code>.</li>
|
|
<li><code>.jsx</code> and <code>.mjs</code> must be served as JavaScript, or module loading fails.</li>
|
|
</ul>
|
|
<p>Asset requests should return 404 when a file is missing rather than falling back
|
|
to the HTML shell. A missing script served as HTML fails in a confusing way.</p>
|
|
|
|
<nav class="more">
|
|
<h2>More documentation</h2>
|
|
<ul>
|
|
<li><a href="/docs/">Documentation</a></li>
|
|
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
|
<li><a href="/docs/architecture/">Architecture</a></li>
|
|
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
|
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
|
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
|
<li><a href="/docs/api/">API Notes</a></li>
|
|
<li><a href="/docs/contributing/">Contributing</a></li>
|
|
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</body>
|
|
</html>
|