Files
securebit-chat/deploy/nginx.conf
T
lockbitchat 113bb107d3
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
feat(groups): audio and video calls in group chats; release v6.6.6
A group call is N-1 ordinary 1:1 calls, one to each other member, each riding
the pairwise session that member already has — a transport a human already
authenticated by comparing the safety code. No mixer, no SFU, no point at which
two people's media meets anywhere but on a device.

Call control is separate from call media, because the two reach different sets
of people. Who opened a call, who joined and who left travels as group frames
signed with the sender's group identity key, so it reaches members currently
reachable only through a relay — and a relaying member can drop one but cannot
write one. Media flows only where a direct link exists, so a member without one
shows as connecting rather than being omitted. Frames carry a per-sender
sequence checked before the action, so a captured leave cannot end a later call,
and simultaneous calls converge on the lower random call id.

One capture is shared across every leg rather than one getUserMedia per member,
and legs answer without prompting: the flag permitting that is set only locally,
only while this user is in the call, and cleared when they leave.

UI: a gallery that sizes itself from the space it has, a spotlight view, an
active-speaker indicator read from the waveform, and the call surface in the
same visual language as the 1:1 one.

Also in this commit, the v6.5.0 language-suggestion work that had not been
pushed yet; its notes are in the changelog. And two fixes: the safety-code input
asks for digits rather than text, and starting a new chat from inside a group no
longer creates it behind the group where it cannot be seen — which had made it
impossible to connect to anyone new, or to add anyone to a group, while a group
was open.

Claude-Session: https://claude.ai/code/session_01XSxAkET3hQTkYDQfbjCQwZ
2026-09-01 01:04:11 -04:00

184 lines
9.2 KiB
Nginx Configuration File

# nginx config for serving SecureBit.chat (static PWA) on Fly.io.
# Mirrors the behavior of the Apache .htaccess: correct JS MIME for ES modules
# (.jsx/.mjs), no-cache for the app shell / service worker / versioning files,
# long-immutable cache for hashed/static assets, security headers, SPA fallback.
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ES modules must be served as JavaScript. nginx's default mime.types maps
# .js but not .mjs/.jsx — declare them explicitly (this overrides .js too).
types {
application/javascript js mjs jsx;
text/css css;
application/json json map;
application/manifest+json webmanifest;
font/woff2 woff2;
font/woff woff;
image/svg+xml svg;
}
sendfile on;
tcp_nopush on;
server_tokens off;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css text/javascript
application/javascript application/json application/manifest+json
application/ld+json application/wasm
image/svg+xml image/x-icon font/woff2;
# Decide Cache-Control from the request path. Keeping all add_header calls at
# one level avoids nginx's header-inheritance reset between blocks.
map $uri $sb_cache {
default "public, max-age=31536000, immutable";
~^/index\.html$ "no-cache, no-store, must-revalidate";
~^/$ "no-cache, no-store, must-revalidate";
~^/sw\.js$ "no-cache, no-store, must-revalidate";
~^/manifest\.json$ "no-cache, no-store, must-revalidate";
~^/meta\.json$ "no-cache, no-store, must-revalidate";
# Operator ICE/TURN config: must never be cached long, or the browser
# locks onto a stale server list (default was max-age=1y immutable).
~^/config/ice-servers\.js$ "no-cache, no-store, must-revalidate";
# dist/ bundles are query-versioned (?v=) in index.html. "no-cache" forces
# revalidation on every load, but dropping "no-store" lets the browser reuse
# the cached copy on a 304 — avoiding a full re-download of the large bundles
# (e.g. qr-local.js ~1.2MB) when nothing changed, while still picking up
# new releases immediately.
~^/dist/ "no-cache, must-revalidate";
# The install prompt is a plain browser module and imports the dictionary by a
# bare path, so that import carries no ?v= to bust the cache with. Left on the
# default one-year immutable rule below, a visitor would keep the translations
# they first loaded for a year.
~^/src/i18n/ "no-cache, must-revalidate";
# Crawler-facing files must stay re-fetchable: the default below is a
# one-year immutable cache, which would freeze a sitemap for a year.
~^/robots\.txt$ "public, max-age=3600";
~^/sitemap\.xml$ "public, max-age=3600";
}
# Which app shell an unmatched navigation falls back to. A request under /de/ must
# land on /de/index.html, not the English one — otherwise every deep link into a
# localized page silently switches language. The entries between the markers are
# rewritten by scripts/build-i18n.js from locales/site.json.
map $uri $sb_shell {
default /index.html;
# BEGIN generated locale shells
~^/de/ /de/index.html;
~^/fr/ /fr/index.html;
~^/es/ /es/index.html;
~^/uk/ /uk/index.html;
~^/ru/ /ru/index.html;
~^/zh/ /zh/index.html;
~^/ko/ /ko/index.html;
~^/hi/ /hi/index.html;
~^/ar/ /ar/index.html;
~^/he/ /he/index.html;
~^/fa/ /fa/index.html;
~^/ur/ /ur/index.html;
# END generated locale shells
}
# CDN-Cache-Control is read by Cloudflare (and other CDNs) *independently* of the
# browser Cache-Control above, and takes precedence at the edge. We force "no-store"
# for the app shell, service worker and version files so a CDN can never serve a
# stale meta.json / index.html / sw.js — which would silently break the in-app
# update notification. The empty default emits no header (nginx skips empty values),
# so immutable hashed assets keep being edge-cached normally for performance.
map $uri $sb_cdn_cache {
default "";
~^/index\.html$ "no-store";
~^/$ "no-store";
~^/sw\.js$ "no-store";
~^/manifest\.json$ "no-store";
~^/meta\.json$ "no-store";
~^/config/ice-servers\.js$ "no-store";
~^/dist/ "no-store";
~^/src/i18n/ "no-store";
}
server {
listen 8080 default_server;
listen [::]:8080 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers (frame-ancestors complements the in-page CSP meta tag).
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none';" always;
# Force HTTPS for two years and preload, closing the first-visit SSL-strip
# window that upgrade-insecure-requests alone does not cover.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Lock down powerful features. Camera + microphone are allowed for QR
# scanning and encrypted voice/video calls; other sensors are denied.
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
add_header Cache-Control $sb_cache always;
# Edge-cache directive for Cloudflare/CDNs (empty value → header is omitted).
add_header CDN-Cache-Control $sb_cdn_cache always;
add_header Service-Worker-Allowed "/" always;
# 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).
location ~* \.(js|mjs|jsx|css|json|map|xml|txt|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ {
try_files $uri =404;
}
# SPA-style fallback so unknown navigation routes still load the app shell.
location / {
try_files $uri $uri/ $sb_shell;
}
}
# One canonical hostname. www.securebit.chat has its own Fly certificate (so
# Cloudflare's origin handshake succeeds), but it must not serve the app as a
# second address — that splits search ranking and canonical links. Cloudflare
# passes the original Host through, so an exact server_name match catches it
# here; everything else falls through to the default server above.
server {
listen 8080;
listen [::]:8080;
server_name www.securebit.chat;
# /sw.js is the one path that must NOT redirect. Visitors who reached www in
# the window between its certificate being issued and this redirect shipping
# registered the site's service worker against this origin, and that worker
# still serves its cached app shell — so those browsers never see the 301,
# while their subresource requests do get redirected and are then blocked by
# the page's own `script-src 'self'`. A 301 here would make that permanent:
# a service worker update fails outright if the script URL redirects. So this
# origin answers with a worker that unregisters itself and reloads the tab.
# See deploy/www-sw.js.
location = /sw.js {
alias /etc/nginx/www-sw.js;
# add_header in a location REPLACES the server-level set rather than
# merging, so the security header has to be restated here.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Re-checked on every navigation instead of being trusted for 24 hours,
# which is how quickly a stuck visitor gets rescued.
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header CDN-Cache-Control "no-store" always;
}
location / {
# The redirect itself must carry HSTS: it is the first response a
# first-time www visitor sees, and the preload list covers subdomains.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
return 301 https://securebit.chat$request_uri;
}
}
}