The palette lived as ~620 hex literals in inline styles plus a few hundred more in
the stylesheets, so there was no single thing to change. It is now 113 custom
properties in src/styles/theme.css, in two blocks.
src/scripts/theme-boot.js decides the theme before first paint — blocking, in <head>,
above the stylesheet, because a deferred script paints dark first and corrects itself.
It stores the mode ('system' | 'light' | 'dark'), never the colour it resolved to, and
stamps data-theme so an explicit choice can beat the media query. The switcher in the
header is a view onto it.
A filled accent stays the brand colour in both themes — the ink on it is near-black
either way — while an accent used as text darkens to clear 4.5:1 on white. A colour
reaches a fill by four routes (a style property, a constant, a helper argument, an SVG
source string), and tests/theme-switching.test.mjs covers all four.
The dark theme is unchanged: every colour declaration the previous build produced comes
out of this one identically once the properties are resolved.
Also: the roadmap drops its status chips on mobile, and Roadmap.jsx no longer splits a
colour with parseInt at runtime, which a var() reference cannot survive.
323 lines
18 KiB
HTML
323 lines
18 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>
|
|
|
|
/* These pages carry no script — that is the point of them — so the theme can only
|
|
come from the media query. There is no toggle here and no stored preference: a
|
|
reference page follows the reader's system and nothing else. The values are the
|
|
same two palettes as src/styles/theme.css, restated because these pages do not
|
|
load the app's stylesheet and are not going to start for eleven declarations. */
|
|
:root {
|
|
color-scheme: dark;
|
|
--d-ink: 255, 255, 255;
|
|
--d-bg: #0f0f11;
|
|
--d-bg-deep: #0b0b0e;
|
|
--d-code-bg: #17171c;
|
|
--d-text: #d6d6dc;
|
|
--d-heading: #f4f4f6;
|
|
--d-strong: #e8e8eb;
|
|
--d-body: #a9a9b3;
|
|
--d-pre: #c9c9d1;
|
|
--d-muted: #8a8a92;
|
|
--d-faint: #6b6b73;
|
|
--d-accent: #f0892a;
|
|
--d-accent-rgb: 240, 137, 42;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
color-scheme: light;
|
|
--d-ink: 0, 0, 0;
|
|
--d-bg: #fbfbfc;
|
|
--d-bg-deep: #f2f3f5;
|
|
--d-code-bg: #f2f3f5;
|
|
--d-text: #26262c;
|
|
--d-heading: #0e0e12;
|
|
--d-strong: #1b1b20;
|
|
--d-body: #43434c;
|
|
--d-pre: #2f3340;
|
|
--d-muted: #63636c;
|
|
--d-faint: #7c7c85;
|
|
--d-accent: #b05c08;
|
|
--d-accent-rgb: 176, 92, 8;
|
|
}
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--d-bg);
|
|
color: var(--d-text);
|
|
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(var(--d-ink),.08);
|
|
font-size: 13.5px;
|
|
}
|
|
.top a { color: var(--d-muted); text-decoration: none; }
|
|
.top a:hover, .top a:focus-visible { color: var(--d-accent); }
|
|
.top .brand { color: var(--d-accent); font-weight: 700; letter-spacing: .04em; }
|
|
.top .here { color: var(--d-text); margin-inline-start: auto; }
|
|
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: var(--d-heading); margin: 0 0 24px; }
|
|
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: var(--d-heading); margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(var(--d-ink),.07); }
|
|
h3 { font-size: 17.5px; font-weight: 700; color: var(--d-strong); margin: 30px 0 8px; }
|
|
h4 { font-size: 15.5px; font-weight: 700; color: var(--d-strong); margin: 22px 0 6px; }
|
|
p, li { color: var(--d-body); }
|
|
p { margin: 0 0 16px; }
|
|
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
|
li { margin: 5px 0; }
|
|
a { color: var(--d-accent); text-underline-offset: 2px; }
|
|
strong { color: var(--d-strong); }
|
|
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: var(--d-code-bg); border: 1px solid rgba(var(--d-ink),.07); border-radius: 4px; padding: 1px 5px; color: var(--d-strong); }
|
|
pre { background: var(--d-bg-deep); border: 1px solid rgba(var(--d-ink),.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: var(--d-pre); }
|
|
.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: var(--d-muted); font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(var(--d-ink),.12); }
|
|
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(var(--d-ink),.06); vertical-align: top; color: var(--d-body); }
|
|
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(var(--d-accent-rgb),.4); color: var(--d-muted); }
|
|
hr { border: 0; border-top: 1px solid rgba(var(--d-ink),.08); margin: 34px 0; }
|
|
img { max-width: 100%; height: auto; }
|
|
a:focus-visible { outline: 2px solid var(--d-accent); outline-offset: 2px; border-radius: 2px; }
|
|
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(var(--d-ink),.08); }
|
|
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: var(--d-faint); 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(var(--d-ink),.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>
|