updates

changelog

major releases and significant changes — minor fixes ship silently

Security audit — XSS patches, server hardening, input validation
fix
XSS — contact custom status
peer-controlled custom status text was injected into innerHTML without escaping. now sanitised with escape_html before rendering.
fix
XSS — enclave file data attribute
base64 file payload from a peer was written into a data-b64 HTML attribute unescaped. any character that breaks out of an attribute context would have been injected. now escaped.
fix
XSS — image lightbox
open_lightbox built its <img> via innerHTML, making the src value an injection sink. rewritten to use document.createElement and .src assignment.
fix
CSS class injection via peer-controlled status
network-received status values were interpolated directly into CSS class names. status is now validated against a strict whitelist (online / idle / dnd / invisible / offline) at the point of receipt and defaults to offline for any unknown value.
fix
CSS selector injection via peer-controlled message IDs
incoming msg_id values were used raw in querySelector attribute selectors. special characters in a malformed ID could corrupt the selector. all msg_id references in selectors now go through CSS.escape().
fix
prototype pollution via enclave role assignment
the server.roles object accepted arbitrary keys from encrypted role-assign packets. a peer sending target_id: "__proto__" could pollute Object.prototype. all role key writes now reject reserved names.
fix
implicit enclave membership via decryptable messages
any peer in possession of the enclave key could silently inject themselves into all clients' member lists by sending an encrypted message. membership is now authoritative from the founder's announce flow only.
fix
CAPTCHA used Math.random()
session-start CAPTCHA characters were selected with Math.random(), which is not cryptographically random. switched to crypto.getRandomValues().
fix
server: WebSocket maxPayload reduced
per-frame payload limit dropped from 32 MB to 4 MB. a single crafted frame could previously consume 32 MB of memory per connection; 5 connections per IP × 32 MB = 160 MB before any rate limit applied.
fix
server: X-Real-IP header validation
the trusted proxy IP header was accepted as-is with no format check. a malformed or multi-value header (e.g. from a mis-chained proxy) could bypass per-IP rate limiting. now validated against a strict IPv4/IPv6 regex.
fix
server: relay URL not exposed to clients
handle_fetch_relays was returning the internal relay_url (ws://localhost:3001 etc.) to all connected clients. internal topology information is now stripped from the relay list sent to browsers.
fix
server: shell injection pattern removed
execSync with a template-literal port string replaced with execFileSync and a separate argument array, eliminating any future shell-injection risk if the port source changes.
fix
server: mailbox registration validates identity format
public_key_hash must now be a 64-char hex string. encrypted_metadata is capped at 4 KB. previously any string could be registered as an identity, enabling memory exhaustion via oversized metadata blobs.
fix
server: security headers applied to /downloads
the download static file handler was registered before the security header middleware, so download responses skipped CSP, X-Frame-Options, and HSTS. headers are now set explicitly on the /downloads route.
fix
server: watcher set growth capped
the per-mailbox status watcher set had no size limit — a flood of watch requests could grow it unboundedly. now capped at 1 000 watchers per mailbox.
fix
word filter entries capped
founder-set word filter patterns are now limited to 100 entries of 50 characters each, preventing pathological regex construction from very long filter strings.
fix
server: queue GC interval reduced
expired queued messages were only garbage-collected once per hour, keeping them allocated for up to 60 minutes past their TTL. GC now runs every 5 minutes.
fix
server: update route validates version format
the /update/win32/:version endpoint now rejects requests with non-semver version strings (must match d.d.d) with a 400 response.
fix
server: uncaught exceptions now logged
previously both uncaughtException and unhandledRejection were swallowed silently, making crypto failures and auth errors invisible. errors are now logged to stderr.
PWA install & QR identity sharing
new
installable PWA
nullcord is now installable as a standalone app on Android and iOS. on Android open the site in Chrome and tap "Add to Home Screen" — on iOS use Safari's share menu. the installed app opens fullscreen with no browser chrome, purple theme, and the orbital icon on your home screen.
new
QR code in identity modal
your identity modal now shows a QR code encoding nullcord.xyz/app?add=YOUR_ID. anyone who scans it is taken straight to the app with your ID pre-filled in the add contact dialog — one tap to send a request, no copy-pasting needed.
new
scan-to-add URL handler
opening the app with ?add=ID in the URL automatically opens the add contact modal with that ID pre-filled. works from QR scans, shared links, or any deep link.
new
copy link button
the identity modal now has a "copy link" action that copies your personal nullcord.xyz/app?add=... add-me URL — shareable anywhere.
new
service worker
a network-first service worker is registered on load, enabling basic offline fallback and satisfying PWA installability requirements. all assets are self-hosted — no external script sources added.
Onboarding tour
new
6-step interactive walkthrough
new users are shown an in-app tour covering how nullcord works — zero-knowledge encryption, ephemeral identities, onion routing, contact flow, calls, and message expiry. each step has an icon, title, and body with progress dots and slide-in animation. the tour appears on first login and can be skipped at any step.
removed
welcome bot removed
the automatic "nullcord" system contact that appeared on first login has been removed. the onboarding tour replaces it — it conveys the same information without cluttering the contact list.
UI redesign, color text, captcha, session management
improve
sidebar redesign
the sidebar top section is now a single compact row — identity badge and actions sit side by side. contact avatars are 34px rounded squares with deterministic per-identity color palettes (purple, cyan, green, red, amber, indigo) derived from the contact's ID hash. section labels and item padding are tighter throughout.
improve
animated orbital auth logo
the auth card logo is now the full animated orbital from the homepage — four concentric rings spinning at different speeds (two clockwise, two counter-clockwise) with a glowing core and ambient float animation.
improve
letter CAPTCHA
the human-verification step now shows five randomized characters (no ambiguous letters like O, 0, I, 1) each rendered in a different accent color with slight random rotation. case-insensitive. mobile keyboards now show the full alphanumeric layout with auto-capitalize.
new
saved session dismiss
the "resume session" banner now has a red ✕ dismiss button. clicking it calls autoauth.clear() — wiping the encrypted identity from IndexedDB and clearing all session flags — so the saved login is gone permanently, not just hidden.
new
colored text markup
select any text in the message input to reveal a floating color toolbar with six accent swatches. picking a color wraps the selection in {#rrggbb:text} markup. messages render the markup as inline colored spans — fully client-side, no server knowledge of formatting.
improve
global custom scrollbar
all scrollbars across the app now use a unified 3px purple-tinted thumb on a transparent track, replacing the scattered per-element browser-default scrollbars that looked inconsistent across surfaces.
removed
security advisory popup removed
the one-per-session security advisory modal that blocked the UI on first load has been removed. the information it contained is available in the changelog and documentation.
removed
terms-of-service gate removed
the scroll-to-agree terms overlay that blocked the app on first visit has been removed. terms remain available at /terms.
fix
logout didn't reset auth UI
logging out left the captcha completed and the resume banner hidden. after logout the captcha now regenerates and the resume check re-runs, so the auth screen is always in a clean state.
Message delivery failures, remove contact, relay error visibility
fix
message delivery failure detection
if a message fails to reach the relay, it is now flagged inline with a red border and "not delivered" label. a retry button re-encrypts and re-sends through the ratchet immediately.
fix
relay error toast
if the server returns a relay_error, a visible error toast is shown — rather than silent failure.
new
remove contact
a remove-contact button in the DM header opens a confirmation modal. confirmed removal deletes all local messages, clears the contact from the sidebar, and revokes cached blob URLs. the removed contact is not notified.
Voice messages, call quality, read receipts, typing indicators
new
voice messages
hold the mic button to record — releasing sends the clip as an encrypted audio file rendered inline with play/pause and a live progress bar.
new
call quality indicator
a live coloured dot in the call bar tracks WebRTC health (green/yellow/red). click to open a debug panel showing RTT, packet loss, jitter, and a live sparkline.
new
read receipts
opt-in encrypted read confirmations. outgoing messages show ✓ (sent) or ✓✓ purple (read). toggle in settings → privacy.
new
typing indicators
sends a lightweight encrypted typing signal while composing (throttled to once per 2.5 s). recipients see a three-dot bounce animation. disappears after 3.5 s of inactivity.
improve
settings modal with tabs
settings are now split into Account, Voice, Privacy, and Danger tabs.
Full-spectrum security hardening
security
sender identity removed from message packets
cleartext sender_id removed from all outgoing packets. sender is now identified only from the ratchet header's ephemeral DH public key — invisible to relays.
before { type:'encrypted_message', sender_id:'abc123', data:{...} } after { type:'encrypted_message', data:{header,iv,ciphertext} }
security
file encryption key moved inside ratchet channel
file metadata (file_key, name, mime, size) previously in cleartext outer packets. now travels inside the ratchet-encrypted channel. outer chunks carry only routing data and encrypted payload.
security
IV replay protection on relay nodes
relay nodes maintain a seen_ivs set. any packet whose AES-GCM IV was processed within the last 60 s is rejected — blocks replay attacks.
security
relay registration restricted to localhost + shared secret
POST /api/relay/register now requires the request originates from loopback and carries a RELAY_SECRET generated at startup. external relay registration is impossible.
security
/api/deliver restricted to localhost
the delivery endpoint now returns 403 for any non-loopback caller. only relay processes on the same machine can inject packets.
security
direct_deliver WebSocket handler removed
a direct_deliver message type allowed any connected client to bypass onion routing entirely. removed. all delivery now goes through relay nodes only.
security
WebSocket payload size cap
128 KB maxPayload limit enforced server-side. oversized frames are rejected before processing.
security
HSTS, COOP, and COEP headers added
Strict-Transport-Security (1 year), Cross-Origin-Opener-Policy: same-origin, and Cross-Origin-Embedder-Policy: require-corp added.
fix
autoauth namespace fallback was static
get_ns() fell back to the string 'd', causing all users without a namespace to share the same IndexedDB prefix. fallback now generates a fresh crypto.randomUUID().
One-time refresh didn't notify group members
fix
beacon didn't include group membership
set_onetime_beacon only stored DM contact IDs. the beacon now also stores group IDs and their member lists so execute_pending_wipe can notify group members on refresh.
fix
beacon not updated on group join/leave
maybe_update_beacon was only called on DM events. now also refreshes on group_created, group_joined, group_left, and group_wiped.
At-rest encryption, relay isolation, one-time identity hardening
  • auto-login encryption key replaced with non-extractable AES-GCM-256 CryptoKey — raw bytes can never be read from IndexedDB
  • relay nodes now support configurable RELAY_URL — can run on separate VPS instances for true hop isolation
  • one-time identities now block backup key export, expiry settings, and auto-login toggle
  • wipe identity clears UI immediately — contact notices are sent in the background
  • group file delivery fixed — onion payload length upgraded from Uint16 to Uint32, express body limit raised to 50 MB, import_state reference error fixed
  • wiped identity now broadcasts group_member_wiped to all group members, not only DM contacts
Multi-account isolation, offline request delivery, file persistence
  • multiple accounts in separate tabs no longer bleed contacts or state — each identity namespaced by its own ID
  • contact and group requests now queued server-side when offline — delivered on reconnect, auto-expired after 24 h
  • groups (messages, members, key) now persist across refresh for timed identities
  • files and images persist across refresh when "store files & media" is enabled — stored locally, encrypted, zero server knowledge
Identity licences, session persistence, security headers
  • one-time licence — fully ephemeral, never written to disk, wipe notices on refresh or tab close
  • timed licence — AES-GCM-256 encrypted IndexedDB storage, auto-destroyed at expiry
  • logout button added — returns to home without destroying identity or autoauth settings
  • security: Content-Security-Policy, Referrer-Policy, X-Frame-Options headers added
  • security: external image rendering removed — prevents tracking-pixel and IP-leak attacks via crafted links
  • terms of service and changelog pages added
Groups, calls, screen sharing
  • encrypted group messaging with per-group AES-GCM-256 symmetric keys
  • group invites — direct or shareable invite links
  • group voice calls over mesh WebRTC
  • group screen sharing with live participant tile overlay
  • key rotation on member removal
  • group expiry — auto-wipe after a set time
Voice calls, screen sharing, file transfer
  • end-to-end encrypted voice calls over WebRTC
  • screen sharing with inline viewer
  • encrypted file transfer with chunk-based delivery
  • identity rotation — generate new keys and notify contacts
  • auto-login — encrypted local key storage
  • configurable account expiry (1h / 1d / 1w / 1mo / never)
Initial release — core messaging
  • anonymous identity generation — no accounts, no sign-up
  • double-ratchet E2E encrypted direct messaging
  • onion-routed message delivery
  • contact requests with accept / reject flow
  • message deletion — either party can remove any message
  • identity self-destruct — wipe all data and notify contacts