Each conversation now runs its own WebRTC session with separate keys and SAS verification, so chats never mix. Adds a side panel to switch between open chats with unread badges, a New chat action that leaves existing chats connected, per-chat local labels stored only on this device, and an availability status (Available, Away, Busy, Invisible) shared end-to-end with connected peers. Also includes vendored Prism syntax highlighting, more reliable PWA update handling, and offline send queueing fixes. Version 4.10.0.
Completes the messaging controls from v4.8.14 and fixes the bug that made them
appear broken for recipients.
Fixed:
- Per-message metadata was silently dropped for recipients. NotificationIntegration
wrapped onMessage and deliverMessageToUI with 2-arg shims that called the
originals without the 3rd argument (meta); with notifications enabled, view-once,
disappearing timers and unsend all failed on the receiving side. Both wrappers
now forward all arguments. Added tests/notification-meta-forwarding.test.mjs.
- Chat would not open after SAS: composer props were threaded into the wrong
component (EnhancedConnectionSetup vs EnhancedChatInterface) -> ReferenceError
nowTick on the verified re-render. Props moved to the chat component.
Changed:
- Code blocks: lightweight dependency-free syntax highlighting via React nodes
(no innerHTML/remote scripts); code mode expands the input; copy auto-clears
the clipboard after ~30s.
- View-once: configurable visible-after-open time (5s/15s/30s/1m) via meta.onceTtl.
- Disappearing timer: duration picker (Off/30s/5m/1h) instead of click-cycling.
- Composer toolbar moved next to "Send files"; borderless buttons, brand-orange
active state; pickers open upward and are mobile-friendly.
- Sender bubble background lightened to rgba(249,115,22,0.05).
Removed:
- Panic wipe button (disconnect already wipes keys and clears session state).
Transport unchanged: per-message metadata travels inside the encrypted envelope,
whitelisted/bounded by _sanitizeMessageMeta. Full suite: 19 files, all passing.
Docs (README, CHANGELOG) updated; version bumped to 4.8.20.
New privacy-focused messaging controls in the composer:
- Code blocks: button wraps the message in a fenced block; both peers render a
monospace code window with a copy button (clipboard auto-clears after ~30s).
Window is built from sanitized text via React nodes — no new XSS surface.
- View-once: recipient sees a blurred bubble, reveals on tap, then it is wiped.
Honestly cooperative (not screenshot-proof).
- Disappearing messages: optional 30s/5m/1h timer auto-deletes on both sides
with a live countdown; incoming TTL clamped to [5s, 24h].
- Unsend (delete for everyone) via new MESSAGE_TYPES.message_delete control.
- Panic wipe: clears chat, wipes keys and disconnects (behind a confirm).
Transport:
- Per-message metadata (id / view-once / timer) travels inside the encrypted
envelope, not in the sanitized text, so content cannot spoof these controls.
- _sanitizeMessageMeta whitelists + bounds metadata on send and receive.
- AAD/replay protection, SAS gate and receive-side DOMPurify are unchanged.
Adds tests/secure-chat-features.test.mjs (full suite: 17 files, all passing).
Bumps version to 4.8.14 across package.json, package-lock.json, manifest.json,
index.html, meta.json, README, SECURITY_DISCLAIMER, header and init banner.
- Remove send-path keyword blocklist that silently rejected legitimate
messages (e.g. "constructor", "global", "document.", literal "javascript:")
without adding protection. XSS is enforced at the rendering boundary by the
receive-side DOMPurify pass and by sanitizeMessage() before encryption.
- Preserve newlines/tabs/indentation in _sanitizeInputString; stop collapsing
all whitespace which destroyed multi-line messages and code snippets.
- Stop logging raw AAD (sessionId + keyFingerprint) on validation failure;
log length only, in both message and file-message AAD validators.
- Add Strict-Transport-Security (2y + preload) and Permissions-Policy
(camera=self for QR, rest denied) to nginx.conf and .htaccess.
- Add tests/outgoing-message-integrity.test.mjs regression suite.
fix(file-transfer): size chunks under the 64KB SCTP message limit
Each 64KB chunk became a ~87KB AES-GCM+Base64 file_chunk message,
exceeding WebRTC's 64KB SCTP message-size floor. The consent handshake
(small messages) succeeded, but no chunk was ever delivered on Safari
and cross-browser connections whose SDP omits a=max-message-size, so
files never transferred. Send chunk size is now 16KB (~22KB on the
wire); inbound chunks up to 64KB stay accepted for backward compat.
fix(file-transfer): make MIME advisory, drive validation by extension
The client-supplied MIME type is easily spoofed and varies across
browsers/OSes, yet was a hard gate: files with an empty MIME or a
cross-OS variant (application/x-zip-compressed, image/jpg) were wrongly
rejected. Extension allow-list plus BLOCKED_EXTENSIONS is now the
boundary; a blatantly foreign MIME on a safe extension is still rejected
and per-type size limits still apply.
- add iceServers.js: allowlist-based validation/normalization of user-supplied
STUN/TURN URLs (rejects javascript:/data:/http/ws, control chars, enforces limits)
- add iceSettingsStore.js: opt-in persistence encrypted at rest with a
non-extractable AES-GCM device key in IndexedDB; load/save/clear
- add IceServerSettings.jsx modal: public vs custom servers, JSON/line input,
live validation, relay-only toggle, 'Test servers' connectivity check,
save-on-device prompt, forget-saved action
- wire chosen servers/privacy mode into EnhancedSecureWebRTCManager construction
(priority: custom > operator override > built-in defaults)
- entry point on the connection-creation screen next to the relay-only toggle
- add ice-servers-validation.test.mjs to the suite
Complete the mandatory receiver-consent gate that was wired in the
backend but never connected to the UI callback chain:
- Add the missing onIncomingFileRequest (4th) callback to
setFileTransferCallbacks in app.jsx — its absence caused
handleFileTransferStart to auto-reject every incoming file.
- Remove independent callback registration from FileTransferComponent;
the component was overwriting app-level callbacks on mount and
nulling all four on unmount, silently breaking progress/received/
error handlers whenever the panel was hidden.
- Lift pendingIncomingFiles state to the root component so consent
prompts are shown regardless of panel visibility; auto-open the
panel on incoming request.
- Add getReceivedFileObjectURL / revokeReceivedFileObjectURL on
EnhancedSecureWebRTCManager for download buttons in the panel.
- Update file-transfer-ui-cleanup regression test to match the new
single-owner callback architecture.
- All 14 tests pass; clean production build.