Peer usernames are remote-controlled and were interpolated into an
innerHTML string in updateLobbyUI. The server only truncates them to 30
chars, so a peer could inject markup into everyone else's popup: enough
to load a remote image (leaking viewer IPs) or spoof readiness badges.
Inline handlers were already blocked by the MV3 default CSP.
Build the peer items with the DOM API, matching the pattern the sibling
peer list already uses.
Add the two checks that would have caught this before upload:
- eslint no-unsanitized, which reproduces the AMO warning at lint time
- addons-linter on the built XPI in verify-release, with
--warnings-as-errors since it exits 0 on warnings and AMO rejects them
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The v2.5.2 Disney+ page-API integration leaked blob-relative <video>
time into force sync, seeks, and heartbeats when the page-API bridge
had no fresh data, so force sync on Disney+ appeared broken.
- getSyncCurrentTime/getSyncDuration now refuse native values on Disney+
(return null/0) so stale bridge data degrades to a clean no-op instead
of broadcasting garbage to peers. The get_current_time handler and
episode/lobby/hcmIsLive paths are routed through the same accessor.
- Validate FORCE_SYNC_PREPARE/SEEK payloads as finite before relaying;
the internal coercion no longer treats '' as 0.
- Stop double-routing FORCE_SYNC_PREPARE from the popup path (the
generic popup route now covers only play/pause/seek).
- popup force-sync: exclude null/empty peer times from the jump-to-others
median (Number(null)===0 was dragging the target to 0), guard against
NaN end-to-end, clear the dangling reset timer on failure, and retry
without re-injecting when the content script responds but the Disney+
bridge has not yet delivered a finite time.
- hcmIsLive skips the native-duration live signal on Disney+ only,
preserving YouTube/Twitch Infinity-duration live detection.
Disney-specific logic remains strictly gated to disneyplus.com; no
Netflix/YouTube/Twitch/generic path is affected.
## Added
- **WebSocket Protocol Specification** (docs/PROTOCOL.md) - Complete reference
for all 20+ events with payload schemas, rate limits, and edge cases
- **LEAVE_ROOM Rate Limiting** - 10 requests/socket/minute to prevent abuse
- **Vitest Testing Framework** - Modern test setup with coverage reporting
- **Host Control Mode Documentation** - Consolidated EN documentation
(docs/host-control-mode.md) with references to internal implementation
## Changed
- **Graceful Shutdown** - Socket.IO clients properly disconnected during
server shutdown (server/index.js)
- **CI Workflow** - Added test step with continue-on-error for gradual rollout
- **README** - Added protocol documentation link
- **CHANGELOG** - Updated with all new features and improvements
## Moved
- Internal documentation moved to docs/internal/ for better organization
- Host Control Mode docs consolidated from 4 files to 1 comprehensive guide
## Technical Details
- Protocol spec: 495 lines, covers all events from shared/constants.js
- Rate limiter: Follows existing pattern (checkAuthRate, checkEventRate)
- Vitest: 6 tests for rate limiter, all passing
- Documentation: Host Control Mode doc includes edge cases, testing checklist,
architecture decisions
- Move KNOWN_LIMITATIONS.md → docs/KNOWN_LIMITATIONS.md (+ fix SECURITY.md link).
- WS test: in default 'everyone' mode a non-host guest can still drive
play/pause/seek/force-sync/episode-lobby — proves host-control OFF == unchanged
behavior.
- docs/host-control-mode-COHOST-PLAN.md: plan for multi-controller (owner grants
drive rights to several co-hosts), built on the capabilities hook; covers the
gate generalization, roles, backwards-compat, edge cases, and a separate
large-room (510-peer) scaling track.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the snap-back robustly instead of deferring it to device testing —
since the set of video sites is unbounded, "measure which players misbehave, then
fix" doesn't scale; the snap-back must be safe on any player.
On an involuntary pause/seek, if the player isn't ready to play (readyState<3 or
seeking) we no longer snap immediately (which fights the buffer: seek → re-buffer
→ pause → … = stutter). Instead hcmDeferredSnapBack polls until the player can
play (8s cap) and then snaps ONCE to the host's re-queried current position. A
player can't play while buffering anyway, so waiting is also more correct, not
just safer. Ready players still snap immediately (no regression).
Guards: aborts if the user goes solo or is no longer a gated guest; single
pending poll (no stacking); cleared in hcmReset.
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>
The host-control desync dialog and the "watching on your own" badge were
hardcoded English — content.js has no i18n loader. Background does, so it now
resolves the strings (GET_HCM_STRINGS: loadLocale + getMessage) and content
fetches them on init with English fallbacks, re-rendering the badge if it was
already shown. The dialog body is now generic (dropped the paused/jumped verb)
to keep translation clean across languages.
Adds 6 keys (HCM_DIALOG_TITLE/BODY/STAY/SOLO, HCM_BADGE_SOLO/RESYNC) translated
across all 15 popup locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hcmHandleBlocked no longer re-checks local control mode before acting. Background
only sends HOST_BLOCKED to a gated guest, so the message itself is authoritative —
adopt host-only/guest role from it. Fixes the join race where a HOST_BLOCKED
arriving before the CONTROL_MODE broadcast was silently ignored.
Also corrects the EC-4 note: "let catch-up re-sync" is invalid (sync is
event-driven, no continuous catch-up); the right fix is a buffer-aware deferred
snap-back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- popup: lock remote-control buttons (Play/Pause/SYNC) for guests in host-only
mode so clicks don't silently get gated and leave the button stuck; backstop
guards in the handlers and the 2.5s safety reset respects the lock.
- content: rebuild the desync dialog + resync badge with the DOM API inside a
Shadow DOM. innerHTML inline style="" attributes are stripped by strict
style-src CSP (Netflix/YouTube/Disney+); CSSOM .style is CSP-safe and the
shadow root isolates from page CSS.
- content: detect live-DVR in hcmIsLive() via a sliding seekable window
(seekable.start(0) > 1), not just duration === Infinity (EC-15).
Records remaining audit items (snap-back thrash, join race, dialog i18n) in the
edge-case log for the device-testing pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All five layers implemented; automated checks green. Records what still needs
real-device testing (EC matrix) and what was deferred by decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 1 of host control mode. Adds the protocol surface only — no behavior yet:
- EVENTS.SET_CONTROL_MODE (client->server: host sets mode)
- EVENTS.CONTROL_MODE (server->client: mode changed)
- CONTROL_MODES { EVERYONE, HOST_ONLY }
Purely additive; old clients ignore the new events. Propagated to
extension/shared via build-extension. Also records the audited design
decisions in the edge-case log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Planning docs for the optional per-room Host Control Mode (Teleparty-style
host-only playback control) requested via GitHub issue.
- host-control-mode-EDGECASES.md: canonical branch entry-point — goals, scope,
trust model, architecture summary, 21 edge cases, test matrix, open decisions.
- host-control-mode-plan.md: layered implementation plan with concrete code hooks
(server room state, three-layer gate, snap-back, guest desync flow, UI/i18n).
Temporary working docs for this branch; to be folded in or removed before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the Ko-Fi support links to https://support.koalastuff.net across the
README badge, the extension footer (popup.html/template.html, label
"Support KoalaSync") and remove the now-unused KOFI_URL constant. Stop
generating/sending the anonymous uninstall token: initUninstallURL() now
registers the feedback URL without the `t` parameter or storage write.
CHANGELOG updated to match.
Also guard the client CMD_ACK path: only ACK a command sender that is still
a known peer in the room, so we don't emit ACKs to peers that already left
(which the server now drops as absent-peer ACKs anyway).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two new languages (uk, zh / zh_CN) across the extension and website:
register them in SUPPORTED_LANGUAGES, the Chrome _locales map, the website
build/lang-init/app routing, hreflang/og/schema tags and language selectors.
Fix systematic machine-translation word-sense errors in the new zh/uk
locales (e.g. zh "Status"=地位→状态, "Leave Room"=留出空间→离开房间,
"Play"=玩→播放, "Seek"=寻找→跳转, audio Attack/Release/Knee; uk "Play"=Грати
→Відтворити, "Clear"=ЯСНО→Очистити, "Open"=ВІДЧИНЕНО→Відкрити, peers
"Однолітки"→"Учасники"), translate remaining English leftovers (Room ID,
Custom) and normalise terminology. All 15 locales pass test-locales.cjs.
Note: popup.html and website/template.html also carry the language-selector
additions here; their Ko-Fi→Support label text is part of the branding change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>