Room-password hashing falls back to a built-in salt that is public in the repo
when SERVER_SALT is not configured. Low risk (the hash never leaves the server),
but operators should set a unique salt — warn at startup, mirroring the existing
ADMIN_METRICS_TOKEN warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Findings recovered from the multi-agent audit (verification phase was cut off by a
usage limit; verified inline against the code):
HIGH:
- Host was demoted on peerId dedup (fast reconnect / second tab): the dedup path
removes the old socket and the kicked socket's 'disconnect' both ran the
host-leave fallback before the same peerId re-joined → room silently unlocked on
every host network blip. Now skip the fallback while a join for that peerId is in
flight (peerJoinLocks). Regression test added. (A long real disconnect still
falls back — that's the deferred host-grace EC-10.)
- Episode auto-advance froze a gated guest: in host-only the guest's EPISODE_LOBBY
is dropped server-side, but the guest still self-paused (PAUSE_FOR_LOBBY) waiting
for readies that never came → 60s freeze. A host-only guest now skips creating a
lobby (the host drives episode sync).
LOW / cleanup:
- GET_HCM_STRINGS could return a raw key name ("HCM_DIALOG_TITLE") if the locale
dictionary failed to load (getMessage returns the key on miss) → omit it so
content keeps its English fallback.
- hcmReset now also clears the snap-back cooldown + buffering grace, so a stale
cooldown can't swallow the first snap-back after a room/host change.
- Popup play/pause labels reset on lock too (not just unlock), so a button can't
freeze on "Playing…" when host-only activates mid-click.
- Fix a stale comment (heartbeat now carries 'desynced').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the implicit "hostPeerId present ⇒ feature supported" heuristic with an
explicit capabilities list the relay advertises in ROOM_DATA. Cleaner, self-
documenting, and the extensible hook for the planned co-host feature (owner
promotes guests to extra controllers) — a future 'co-host' capability + events
slot in without a protocol bump.
- shared: CAPABILITIES { HOST_CONTROL }.
- server: SERVER_CAPABILITIES advertised in ROOM_DATA.
- background: track serverCapabilities (empty against older relay), serverSupports(),
thread hostControlSupported through GET_STATUS / GET_CONTROL_MODE / CONTROL_MODE.
- popup: gate the host-control card on the explicit capability instead of hostPeerId.
Backwards-compatible both ways: old relay omits the field → feature stays hidden
(no errors); old client ignores the field. WS test asserts ROOM_DATA advertises
the capability. Adds docs/host-control-mode-TESTING.md (beta-server setup, wss
caveat, two-new-clients note, verification checklist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bis auf peerId-Auth (akzeptiert) alle Audit-Findings behoben:
- Snap-back spielt nicht mehr blind ab, wenn Host-Status unbekannt
- Teardown-Pfade broadcasten jetzt CONTROL_MODE → kein verwaistes Badge/Dialog
- Dialog-Timer bei Ersetzung sauber gecleart
- Server re-synchronisiert Sender bei nicht-Host-Reject
- Popup-Toggle revertiert bei Server-Ablehnung
- Desync-Zweig sendet keine Phantom-ACKs mehr an Host
- Toggle-Debouncing serverseitig (500ms pro Raum)
- Button-Text auf Unlock zurückgesetzt
- Live-Erkennung defensiver, Resync-Retry bei fehlendem Target, Badge-Retry
Neu: Host sieht 'Solo'-Badge für desyncte Gäste via PEER_STATUS-Heartbeat
(16 Sprachen, neuer i18n-Key BADGE_DESYNCED/TOOLTIP_PEER_DESYNCED)
Tests: H-5-Reject-Unicast, M-4-Debounce, Desync-Heartbeat-Relay
Step 2 of host control mode (server-side, fully testable without UI):
- Room now tracks hostPeerId (= first joiner) and controlMode ('everyone' default).
- ROOM_DATA carries hostPeerId + controlMode so clients know their role on join.
- SET_CONTROL_MODE handler: host-only authority check, broadcasts CONTROL_MODE.
- Relay gate: in host-only mode, drop room-moving events (play/pause/seek/
force-sync/episode-lobby) from non-host guests — robust chokepoint that kills
spam regardless of client. Heartbeats/ACKs/episode-ready still pass.
- removePeerFromRoom: if the host leaves, fall back to 'everyone' and reassign
host to the earliest remaining peer (v1: immediate, no grace period).
Extends the WS integration test suite to cover all of the above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Raise the per-socket event budget from 30 to 50 per 10s to give legitimate
bursts (episode joins, force-sync, ACK flurries) more headroom. Extract the
previously inline connection/event/health/admin windows and limits into named
constants (CONNECTION_RATE_LIMIT, EVENT_RATE_LIMIT, *_WINDOW_MS). Tests now
derive their thresholds from the exported constants instead of hardcoding them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Media players fire bursts of native play/pause events (source swaps, ABR,
ads, teardown). Each was relayed as a distinct command, spamming peers and
tripping the relay's event rate limit. Add leading+trailing coalescing in
the content script: emit the first event instantly (zero added latency) and,
on a 150ms window, collapse a burst to its final state. Echo-suppression and
seek-flush stay synchronous on event arrival; the trailing send is never
deduped against the leading one (a remote command may change shared state
mid-window — re-sending is the safe, idempotent choice).
Server: split the EVENT_ACK handler's logging so only a genuine different-room
ACK is logged as [SECURITY]; an ACK to a peer that already left is logged
quietly as [ACKDROP] (verbose-only), so real signals are no longer drowned out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move 6 rate-limit functions, all rate-limit Maps, cleanup intervals,
and denial counter to server/rate-limiter.js. 149 lines extracted.
Index.js re-exports what tests and ops.js need.
npm run verify passes.
The previous fix had a subtle bug: JavaScript && short-circuits,
so crypto.timingSafeEqual() was skipped when buffer lengths differed
(sameLength was false). The dummy buffer was allocated but never
compared, leaving the original length-based timing leak intact.
Now timingSafeEqual is eagerly assigned to a const before the &&
guard, guaranteeing it runs in constant time on every auth attempt
regardless of whether the length guess was correct.
isAdminMetricsAuthorized() returned early when buffer lengths differed,
allowing an attacker to discover the token length by measuring response
time. Now always calls crypto.timingSafeEqual (using a zeroed dummy
buffer on length mismatch) so every auth attempt takes constant time
regardless of whether the length guess was correct.
Reported-by: Kaia-Alenia
Adds application-level ping/pong between extension and relay server.
Extension sends PING every 15s, server echoes PONG. Round-trip time
displayed in Status tab (green/yellow/red color-coded).
Server also forwards PING with target peerId and routes PONG back,
enabling future peer-to-peer ping without server restart.
Extension already responds to incoming peer PINGs.
See CHANGELOG.md for details.
- FORCE_SYNC_TIMEOUT in shared/constants.js was 5000 but all code uses
8500ms. Update constant to 8500 and reference it in background.js
instead of hardcoded values (4 occurrences)
- Add errorToken counter in popup showError() so stale 5s timeout
doesn't clear styling of a newer error that arrived in between
- Fix EVENT_ACK handler indentation in server/index.js to match
surrounding socket.on handlers
- Store real client IP from x-forwarded-for on socket for use in JOIN_ROOM
auth rate limiting (was using proxy IP, breaking brute-force protection)
- Remove clearEpisodeLobbyState() from socket.onclose to preserve lobby
across brief disconnects; ensureState() recovers lobby + timeout on reconnect
- Lobby is still properly cleared on intentional LEAVE_ROOM and room switch
Two parallel JOIN_ROOM to a non-existent room could race past each
other during bcrypt.hash, causing the second to overwrite the first
room (password hash lost). The lock was read but never written.
- Create lock promise before bcrypt.hash async boundary
- Release in finally to cover all exit paths (success, MAX_ROOMS, error)
- Concurrent waiters now correctly await existing room creation
Server Security (S-1 through S-8):
- S-1: Type-check and clamp peerId, protocolVersion, password
- S-2: Validate numeric/boolean/enum fields in relay peerData
- S-3: Construct explicit relay payload (stop spreading raw data)
- S-4: Type-check targetId and actionTimestamp in EVENT_ACK
- S-5: Restrict room IDs to [a-zA-Z0-9-] only
- S-7: Add eventCounts periodic cleanup alongside connectionCounts
- S-8: Guard version parsing against NaN bypass
Documentation (P-1, R-1 through R-6):
- P-1: Fix PRIVACY.md typo, document all in-memory data maps
- R-1/R-5: Fix stale sync-constants.bat references in shared/
- R-2: Fix stale lastTargetState ref in ARCHITECTURE.md
- R-3: Extension README title reflects cross-browser support
- R-6: Document content injection markers in scripts/README.md