From fb4329dd941d02c8dd6a923de618368d91e403ab Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:37:20 +0200 Subject: [PATCH] release: v1.2.0-alpha.3 (#1377) Bump the client version in package.json (+lock), tauri.conf.json and Cargo.toml (+lock) so release.yml's verify-versions gate passes and deployed clients see the update; refresh the literal version in the README and docs build examples; add the curated CHANGELOG entry covering the 199 verified defects fixed since v1.2.0-alpha.2 (#1366-#1375), the observability/backup/deployment hardening in #1376, migration 031, and the new config keys. Co-authored-by: Claude Fable 5 --- CHANGELOG.md | 229 ++++++++++++++++++ Client/tauri-client/package-lock.json | 4 +- Client/tauri-client/package.json | 2 +- Client/tauri-client/src-tauri/Cargo.lock | 2 +- Client/tauri-client/src-tauri/Cargo.toml | 2 +- Client/tauri-client/src-tauri/tauri.conf.json | 2 +- README.md | 4 +- docs/api.md | 2 +- docs/deployment.md | 4 +- docs/quick-start.md | 4 +- 10 files changed, 242 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e83cf18..11bf03be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,235 @@ tooling (`npm run changelog`) auto-generates entries from commit messages on each release; this file is the curated counterpart that calls out behavioural changes operators must know about. +## v1.2.0-alpha.3 + +- **fix:** eight bug-hunt batches closed **199 verified defects** since + `v1.2.0-alpha.2` — 30 in #1366/#1367, 110 in #1369–#1372, 34 in #1374 and + 25 in #1375 — each fixed test-first with the failing assertion watched red + against the unpatched code. The behavioural consequences worth knowing + about are listed below; the rest are one-line correctness fixes with no + operator-visible change. +- **security(client): voice E2EE was never actually enabled** (#1370). The + full ECDH/HKDF/AES-GCM key exchange completed, the room key was set, and + the UI showed 🔒 Secured — but `createRoom` never called + `room.setE2EEEnabled(true)`, so every audio and video frame reached the + SFU in plaintext. It is enabled now, and a dead E2EE worker is no longer + invisible to the Secured badge. Related voice-crypto fixes: a joining key + holder sent its room-key offers *before* its own announce, so existing + participants dropped them as "unknown peer" (#1370, #1374); rotation + offers exceeded the server rate limit in large channels and permanently + starved the same peers; both rotation paths and the reconnect-to-Secured + path now carry session-generation guards; a departing peer's ephemeral + key is retired on leave so a replayed pre-leave announce cannot overwrite + the fresh key they rejoined with (#1372, #1374). The client also refreshed + its LiveKit token every 23 hours while the server mints it with a 5-minute + TTL, so auto-reconnect failed for any voice session older than five + minutes (#1370). +- **security(server):** access-control holes (#1369–#1372, #1374, #1375) — + `voice_join` into a 1:1 DM had no block gate, so a blocked user could + enter the blocker's DM voice room; the attachment-serve admin bypass let + an ADMINISTRATOR download files from private DMs they were not in; the + archived-channel read-only gate covered `SendMessage` only, so edit, + reaction, pin, purge, delete and `channel_focus` still mutated or + subscribed to archived channels (every write sink now routes through one + `requireChannelWritable` gate); `EditMessage` and `handleReaction` DM + detection failed *open* on a `GetChannel` error, skipping the block gate; + group-DM creation only block-checked the creator, letting a third party + force two users who blocked each other into a shared room; an invisible + user's real custom status leaked on both presence emitters; `PATCH + /users/{id}` with `banned` + `role_id` committed and broadcast the ban + before authorizing the role change; admin API-token creation accepted a + negative `expires_hours` and minted a token that never expires; upload + rejections echoed raw storage errors (absolute server paths) to any + authenticated user; the GIF proxy's log redaction missed the + percent-encoded API key; `chat_command` was the only client message type + without a rate limiter while each frame ran a WASM plugin invocation; and + the login and typing rate limiters built their keys from *unvalidated* + input, letting an unauthenticated caller pin unbounded heap for six hours. +- **fix(auth):** accounts whose username contains `'`, `"` or `&` were + permanently unloggable — registration HTML-escaped the name but login did + not — and a profile rename to such a name locked the user out (#1370). If + you had users hit this, they can log in again with no action on your side. + Also: message search returned 500 for any query containing a hyphen (the + one FTS5 operator the sanitizer allowlisted); usernames with an uppercase + non-ASCII letter could never be @mentioned; registration recorded the + reverse-proxy address as the session IP. +- **server:** WS hub, reconnect and replay (#1369, #1371, #1372, #1374, + #1375) — REST DM events never bumped the visibility watermark, while + *every* ordinary DM message re-emitted `dm_channel_open` and bumped the + global watermark, forcing every other client's next reconnect into a full + resync; the client's `lastSeq` was never reset by a full-ready resync and + desynced permanently; cold-tier replay had no interior-gap detection, so + events the persister dropped were skipped and presented as a complete + resume; `buildReady` swallowed three DB errors and shipped an + authoritative-looking empty snapshot (the client wiped its DM list, member + list and unread badges) and dropped the user's own live voice room when + not READ-visible; `channel_focus` could re-subscribe after a concurrent + visibility revoke, and role demotion's live-subscription revocation was + gated on a cosmetic role re-read; a failed reconnect handshake ran the + full disconnect teardown twice; presence events from every source now + share one ordered per-client FIFO. +- **server:** voice lifecycle (#1369, #1371, #1374) — a stale join's + rollback deleted `voice_states` by user id alone, destroying a concurrent + newer membership; deleting a voice channel raced a concurrent `voice_join` + into a permanent hub/SFU ghost no sweep could heal; the stale-state sweep + could delete a just-committed join's row, leaving the client in voice with + no DB row; `handleVoiceJoin` handed out a live 5-minute LiveKit credential + *after* a concurrent kick/move/revocation had already torn the membership + down (the token is now withheld); the `participant_left` webhook never + told the leaver, and a transient DB read error on `participant_joined` + ejected a legitimate participant mid-call; `voice_mod_move` lacked the + archived-channel gate; `CleanupVoiceForChannel` resolved an empty + `voice_leave` audience because both callers archive first. Camera and + screenshare now draw from the same per-channel `voice_max_video` budget — + screenshare had no cap check at all, and the camera gate did not count + screensharing occupants. +- **server:** DM and message fan-out (#1369, #1371, #1372, #1375) — a DM + send, edit, delete or reaction survived a transient participant lookup + failure by silently dropping live fan-out to everyone including the + sender; emoji create/delete and group-DM creation tied their broadcasts to + the request context, so an aborted request committed the mutation and + skipped the event; slow mode consumed its cooldown token before content + validation, so a rejected send locked the composer for the full window; an + attachment-metadata read failure broadcast the message with no + attachments; `GET /channels/{id}/pins` had no LIMIT and failed permanently + past ~32k pins; pinning a soft-deleted message returned 500; + `LinkAttachmentsToMessage` no longer claims a user's live avatar as a + message attachment; `PATCH /channels/{id}` now rejects a blank name. +- **server:** admin and plugins (#1369, #1370, #1372, #1375) — "Restore + backup" wrote to a hardcoded `data/chatserver.db`, so it silently no-oped + on any server with a configured `database.path`; the WAF inline engine + rejected every request body ≥ 1 MiB, breaking plugin install and large + avatar uploads when `waf_enabled` was on; self-account-deletion emitted no + `member_ban`, so every other client kept the deleted user; the admin live + log stream blanked every error attribute to `{}`; `CheckForUpdate` had no + in-flight dedupe and stampeded GitHub on cache expiry; a failed self-update + swap left every client counting down to a restart that never came (a + corrective `update_aborted` is now broadcast, and deferred cleanup runs + before the restart exits — on Windows that file-handle release is the + reason the restart exists). Plugin enable/re-install left + `plugins.enabled = 1` while the runtime instance was deactivated, and + uninstall reported success while the on-disk directory survived and + resurrected the plugin on the next start. +- **fix(client):** voice reliability (#1366, #1367, #1370–#1372, #1374, + #1375) — a failed voice channel-switch left the user live in the call + (mic hot, audio flowing) with the voice UI hidden and no way to leave; + selecting the "Default" microphone (or losing the pinned one to a hot + unplug) never changed the capture device; a camera or screenshare disable + that completed while the enable's `publishTrack` was in flight left the + server and every peer believing it was on (`leaveVoice` and reconnect + teardown now bump the same generation guard); a `VIDEO_LIMIT` rollback + assumed the camera and tore down a working camera while leaving refused + screen tracks published — it now correlates by envelope id; auto-idle's + return-to-online `presence_update` was always swallowed by the 1-per-10s + limiter, so every user showed Idle to everyone else after their first idle + period; connection-quality degradation was never reported; a group-DM + decline silenced every other participant's ring and never reached the + caller. +- **fix(client):** messaging and stores (#1366, #1367, #1369, #1372) — + re-opening a channel visited earlier in the session rendered a permanently + stale window (live broadcasts only cover the focused channel; the tail is + now refetched); the virtual scroll window never followed the scroll + position, so rows past the initial overscan rendered as blank space; a + scroll-up page past the 500-row cap deleted the user's pending/failed + rows, the only copy of their composed text; the scroll-to-bottom button + and "Jump to Present" pill scrolled out of view exactly when they became + visible; a user named exactly "System" had every message rendered as a + server notice with no moderation controls; DM permalinks failed until the + DM had been opened once; the reaction picker dropped the server's custom + emoji; Ctrl+K was dead with CapsLock on; the composer's slow-mode cooldown + was applied to whichever channel was mounted, not the one that sent. +- **fix(client):** settings, session and platform (#1367, #1370–#1372, + #1375) — the built-in light theme overrode only 4 of ~45 tokens (composer + and inputs near-invisible), the Font Size slider and High Contrast toggle + were no-ops, and the tray Status menu bypassed the client's own status + state so a tray-set Do Not Disturb silenced nothing; a failed TOTP verify + tore down the overlay so the code could not be re-entered; channel + create/edit/delete modals locked up permanently on an API failure; login + to an IPv6-literal host was impossible; a host stored with an explicit + `:443` lost its bearer token and cert-pinned proxy on attachment fetches; + one malformed stored server profile discarded *all* saved profiles; a + banned/revoked token reconnected forever if the session ended before + MainPage mounted; a previous server's block list, collapsed categories and + DM notes bled into the next server; the Rust HTTP proxy tunnel's data + phase had no deadline, so a remote that completed TLS then went silent + parked the connection forever (bounded at 600s — loose on purpose, this + path carries uploads); the autostart toggle raced its own write. +- **infra:** observability, backups, guardrails and deployment hardening + (#1376). **`/health` now returns a real verdict** — hub dispatch-loop + liveness, a bounded DB ping and a free-disk check, answering **503 with a + subsystem reason** (`hub`, `database`, `disk`) when degraded; results are + cached so the unauthenticated endpoint cannot amplify load. Point uptime + monitors at it and treat any 503 as actionable. **The hub's panic breaker + now exits the process** so a supervisor can restart it, instead of leaving + broadcast delivery silently dead while clients still appear online — if + you run the bare binary without a supervisor, use the new hardened + `deploy/owncord.service` systemd unit (see "Running as a Linux Service"). + **Backups now actually run:** `backup_schedule` and `backup_retention` + had existed in the admin panel since the initial schema but were never + read by any code; the 15-minute maintenance loop now enforces them, + verifies each backup with `PRAGMA integrity_check` (and again before a + restore may overwrite the live DB), and prunes by age keeping the newest. + Expect backup files to start appearing and pruning for the first time. + `/api/v1/metrics` gains reconnect-tier, backpressure, DB-writer-wait, + permission-cache, `ws_conn_rejects` and `disk_free_mb` signals, and the + declared-but-never-recorded OTel instruments are wired. Upload storage + failures return **507** instead of blaming the client with a 400. A + single-process lock beside the SQLite file makes a second server process + fail fast instead of silently fighting the first. **Unknown config keys + now warn at startup** (a typo previously kept the default silently), and + startup warns when `admin_allowed_cidrs` is customized while + `trusted_proxies` is empty. Shutdown now joins the pruner and maintenance + loop before the DB closes, drains HTTP handlers into a live hub, and skips + the 5s client-notice window when nobody is connected. Write-path work: + no-op read-state UPSERTs are skipped, boot-time `ANALYZE` runs only when a + migration applied, role-scoped override changes evict only that role's + members from the permission cache, and connect/disconnect presence passes + through a 300ms latest-wins coalescer (wire format and seq ordering + unchanged). +- **config:** new keys, all defaulting to current behaviour (#1376) — + `server.max_ws_connections` (0 = unlimited; over the cap answers 503 + + Retry-After), `server.metrics_allowed_cidrs` and + `server.livekit_webhook_allowed_cidrs` (both fall back to + `admin_allowed_cidrs`, so a central Prometheus scraper or an + externally-hosted LiveKit no longer requires widening the admin + perimeter), `database.max_readers` (0 = auto), `backup.dir` + (`data/backups`), `security.auth_rate_limit_multiplier` (1.0; raise for + shared-NAT communities), `event_persistence.replay_ring_size` (1000) and + `event_persistence.replay_cold_limit` (5000 — watch `reconnect_tier_full` + before raising). Three stored-but-inert admin settings (`server_icon`, + `max_upload_bytes`, `voice_quality`) are now shown read-only with a + pointer at the real `config.yaml` keys instead of pretending to apply. + Documented in `docs/server-configuration.md`. +- **deploy:** new `chatserver healthcheck` subcommand probes `/health` + pinning the server's own certificate from disk (WebPKI when none exists, + i.e. ACME) and is now the docker-compose healthcheck — the distroless + image has no shell; plain `docker compose` only *surfaces* unhealthy, pair + it with a watchdog for auto-restart. Compose gains json-file log rotation + (`10m` × 3) on both services. `release.yml` now cold-boots the freshly + built server binaries and Docker image and probes them healthy **before + anything is signed or pushed** — the release feed drives signed + self-updates, so a binary that compiled but died on boot would previously + have shipped itself to every auto-updating instance. New "Reverse Proxy + Topology" docs section (nginx snippet; only WebRTC media ports need to be + directly reachable, `/livekit/*` is already proxied). Release binaries + are built with Go 1.26.6 (stdlib CVE fixes flagged by govulncheck). +- **migrations:** **031** normalizes legacy `sessions.expires_at` values to + RFC3339-UTC and adds `idx_sessions_expires_at`, so the 15-minute expired- + session sweep is an index lookup instead of a full-table scan on the + writer. Applies automatically on first start; no operator action needed. +- **protocol:** no wire changes — `docs/protocol-schema.json`, + `message_types.go` and `protocolTypes.ts` are byte-identical to + `v1.2.0-alpha.2`. Older clients and servers interoperate unchanged. +- **fix(ws):** the LiveKit health check shared the process-wide + `http.DefaultTransport` pool with every other user in the server; it now + owns a private transport (#1356). +- **chore:** bug-hunt tooling under `.claude/` (fix pipeline, findings + ledger, circuit breaker, single-finder hunt with graph-fed targeting — + #1361–#1365, #1373); dependency bumps (OTel 1.45.0, koanf, sqlite, + eslint/oxlint/knip/typescript-eslint, tauri-plugin-updater, GitHub + Actions; #1353–#1360). No runtime impact. + ## v1.2.0-alpha.2 - **feat(client):** the login form has an **Auto connect** checkbox under diff --git a/Client/tauri-client/package-lock.json b/Client/tauri-client/package-lock.json index e25fa83e..7d8ed27f 100644 --- a/Client/tauri-client/package-lock.json +++ b/Client/tauri-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "owncord-client", - "version": "1.2.0-alpha.2", + "version": "1.2.0-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "owncord-client", - "version": "1.2.0-alpha.2", + "version": "1.2.0-alpha.3", "dependencies": { "@jitsi/rnnoise-wasm": "^0.2.1", "@tauri-apps/api": "^2.10.1", diff --git a/Client/tauri-client/package.json b/Client/tauri-client/package.json index c3603d5f..4b96f255 100644 --- a/Client/tauri-client/package.json +++ b/Client/tauri-client/package.json @@ -1,7 +1,7 @@ { "name": "owncord-client", "private": true, - "version": "1.2.0-alpha.2", + "version": "1.2.0-alpha.3", "type": "module", "scripts": { "dev": "vite", diff --git a/Client/tauri-client/src-tauri/Cargo.lock b/Client/tauri-client/src-tauri/Cargo.lock index 7b32ce1a..78868ca0 100644 --- a/Client/tauri-client/src-tauri/Cargo.lock +++ b/Client/tauri-client/src-tauri/Cargo.lock @@ -3021,7 +3021,7 @@ dependencies = [ [[package]] name = "owncord-client" -version = "1.2.0-alpha.2" +version = "1.2.0-alpha.3" dependencies = [ "base64 0.22.1", "device_query", diff --git a/Client/tauri-client/src-tauri/Cargo.toml b/Client/tauri-client/src-tauri/Cargo.toml index 2586b9c5..280db4e9 100644 --- a/Client/tauri-client/src-tauri/Cargo.toml +++ b/Client/tauri-client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "owncord-client" -version = "1.2.0-alpha.2" +version = "1.2.0-alpha.3" edition = "2021" # Effective minimum: tauri 2.11 declares rust-version = "1.77.2", so the crate # cannot build below it. Declaring it here enables Cargo's MSRV-aware resolver diff --git a/Client/tauri-client/src-tauri/tauri.conf.json b/Client/tauri-client/src-tauri/tauri.conf.json index 3568e03f..7c7e8899 100644 --- a/Client/tauri-client/src-tauri/tauri.conf.json +++ b/Client/tauri-client/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "OwnCord", - "version": "1.2.0-alpha.2", + "version": "1.2.0-alpha.3", "identifier": "com.owncord.client", "build": { "frontendDist": "../dist", diff --git a/README.md b/README.md index eff01f91..39a19f94 100644 --- a/README.md +++ b/README.md @@ -138,11 +138,11 @@ Two main components: ```bash # Server (Windows) cd Server -go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.3" . # Server (Linux) cd Server -CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.3" . # Client cd Client/tauri-client diff --git a/docs/api.md b/docs/api.md index 30e22ad0..fcdd3880 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2017,7 +2017,7 @@ Owner-only self-update from GitHub Releases (minisign/Ed25519-verified; see ```json { - "current": "v1.2.0-alpha.2", + "current": "v1.2.0-alpha.3", "latest": "v1.2.0", "update_available": true, "required_assets_present": true, diff --git a/docs/deployment.md b/docs/deployment.md index fc213988..cf3c4040 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -16,13 +16,13 @@ Production deployment guide for OwnCord server on Windows and Linux. **Windows:** ```bash cd Server -go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.3" . ``` **Linux:** ```bash cd Server -CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.3" . ``` - `-s -w` strips debug info (smaller binary) diff --git a/docs/quick-start.md b/docs/quick-start.md index 1ed92b27..44925155 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -55,11 +55,11 @@ Full Docker details: [Deployment Guide](deployment.md#docker-linux). ```bash # Server (Windows) cd Server -go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +go build -o chatserver.exe -ldflags "-s -w -X main.version=1.2.0-alpha.3" . # Server (Linux) cd Server -CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.2" . +CGO_ENABLED=0 go build -o chatserver -ldflags "-s -w -X main.version=1.2.0-alpha.3" . # Client cd Client/tauri-client