websockets.connect() used default SSL verification, rejecting self-signed
certs on remote instances. Same issue as the httpx fix (2c21dfe) but for
the websockets library. Fix: pass ssl.SSLContext with CERT_NONE when the
remote URL is wss://.
Our custom handler sent clipboard text via WebSocket, but the browser's
native paste event also fired on xterm.js's hidden textarea, causing
xterm's built-in paste handler to send the same text again. Fix:
e.preventDefault() suppresses the browser paste event.
The source-inspection test correctly caught one remaining unguarded
Authorization header in the WS proxy additional_headers. Applied the
same 'if remote_key else {}' pattern as the other 4 federation endpoints.
Clicking an unreachable remote device tile passed empty session name
to openSession(), producing /api/federation/2/connect/ (no session name)
→ 405. Fix: bail early if name is empty or whitespace.
Also added guard in grid click handlers to skip error/status tiles
entirely, preventing clicks on unreachable or auth_failed tiles.
Empty remote_instances[].key produced 'Bearer ' (illegal header value)
causing httpx to reject the request entirely. Fix: only include the
Authorization header when the key is non-empty. Remotes without keys
still work if their auth allows it (e.g., localhost bypass).
Fixes all 5 sites: fetch_remote GET, connect POST, bell/clear POST,
create-session POST, and WebSocket proxy additional_headers.
httpx.AsyncClient used default SSL verification, rejecting self-signed
certs from muxplex setup-tls. Remote HTTPS instances (e.g., setup-tls
with self-signed fallback) were unreachable — all federation requests
failed with CERTIFICATE_VERIFY_FAILED. Fix: verify=False on the
federation client. Bearer token auth still protects authorization.
blur handler on the name input was closing the entire new-session UI
when focus moved to the device select. Fixed: check if activeElement
is the sibling select before running cleanup. Same guard added to the
select's blur handler (check if focus returned to input).
Also adds Escape keydown handler on the select to allow cancelling
without returning focus to the input first.
Applied to both showNewSessionInput() and showFabSessionInput().
11 tests calling main() failed in GitHub Actions because ttyd is not
available in standard Ubuntu packages. _check_dependencies() calls
sys.exit(1) when ttyd is missing. Fixed by adding a mock_check_deps
pytest fixture and applying it to all 11 affected tests.
Affected tests:
- test_main_calls_serve_by_default
- test_main_passes_custom_host_and_port
- test_main_default_host_is_localhost
- test_main_passes_auth_flag
- test_main_passes_session_ttl_flag
- test_main_passes_none_for_unset_flags
- test_main_passes_explicit_host_only
- test_main_serve_subcommand_accepts_flags
- test_main_passes_tls_cert_and_key_flags
- test_main_passes_none_for_unset_tls_flags
- test_serve_subcommand_accepts_tls_flags
- Add contents: read permission to publish.yml (required for actions/checkout)
when any permission is explicitly set, unlisted permissions default to none
- Add timeout-minutes: 15 to publish.yml for consistency with ci.yml
- Add docstrings to 3 pyproject metadata test functions for consistency
with the rest of the 2000+ line test file
- Add .github/workflows/ci.yml with correct placement in muxplex repo
- Matrix: Python 3.11, 3.12, 3.13 with fail-fast: false
- Uses uv for Python and dependency management with package caching
- Runs pytest via uv run pytest -v (config in pyproject.toml addopts)
- timeout-minutes: 15 to prevent runaway CI costs
- permissions: contents: read for minimal GITHUB_TOKEN scope
Co-authored-by: Amplifier <amplifier@example.com>
The 5 pre-existing tests used \(\w+\) (single-param regex) but Task 4
changed createNewSession(name) → createNewSession(name, remoteId). Updated
regex to \([\w,\s]+\) to match one or more comma-separated parameters.
- Update showFabSessionInput to call _createDeviceSelect() for optional
device <select> element
- Append select (if present) then input to overlay
- Enter handler reads remoteId from select.value, passes to createNewSession
- Add test: showFabSessionInput creates device select when multi_device_enabled with remotes
- Add _createDeviceSelect() helper function after _createSessionInput()
- Returns null when multi_device_enabled is false or remote_instances is empty
- Creates <select class='new-session-device-select'> with local + remote options
- Pre-selects based on _activeFilterDevice match
- Update showNewSessionInput() to call _createDeviceSelect() and insert select
- cleanup() removes both select and input; restores btn display
- Enter handler reads remoteId from select.value, calls createNewSession(name, remoteId)
- Export _createDeviceSelect for testing
- Add 3 tests covering select creation, integration, and argument passing
Adds federation_create_session endpoint following the same pattern as
federation_connect and federation_bell_clear. The endpoint:
- Looks up remote by integer index into remote_instances in settings
- Sends POST {remote_url}/api/sessions with Bearer auth header and
JSON body {name: ...}
- Returns remote's JSON response
- Raises 404 for invalid remote_id, 503 when unreachable, 502 on HTTP error
Adds 4 tests covering:
- Proxy behavior with correct URL, auth header, and JSON body forwarding
- 404 for out-of-range remote_id
- 503 when ConnectError raised
- 502 when remote returns HTTP error status
Update updatePillBell and updateSessionPill to build a viewingKey
that accounts for remote sessions (_viewingRemoteId prefix) and
compare using s.sessionKey || s.name instead of s.name alone.
This prevents false bell suppression when a local session shares a
name with a remote session — the two are now distinguished by their
fully-qualified key (remoteId:name vs plain name).
- Remote sessions now include sessionKey field formatted as 'remoteId:name'
- Local sessions are unchanged (no sessionKey) as they never collide
- Prevents name collisions between local and remote sessions with identical names
- Added tests: test_federation_sessions_local_sessions_have_no_session_key
test_federation_sessions_remote_sessions_have_session_key
Favicon badge: removed crossOrigin='anonymous' from _drawFaviconBadge Image
lazy-init. Same-origin favicons don't need CORS credentials; setting it caused
silent cache-miss failures when the browser had the asset cached without CORS
headers, preventing the canvas from drawing.
Page title: new updatePageTitle() centralizes title logic. Format:
'(N) hostname - muxplex' when N sessions have unseen bells, otherwise
'hostname - muxplex'. Hostname uses device_name setting with fallback
to location.hostname. Called from pollSessions on every tick, from
loadServerSettings callback, and immediately when device_name input changes
(updating _serverSettings.device_name in place first so the helper sees the
latest value without waiting for the debounced PATCH).
Replaced 3 scattered document.title assignments with updatePageTitle().
Updated 3 existing source-pattern tests to reflect the new centralised
approach; added 2 new tests for updatePageTitle existence and pollSessions
call site.
When host is set to network access (not 127.0.0.1) and TLS is not
configured, doctor shows 'Run: muxplex setup-tls' and service install
shows a tip line. Hidden on localhost-only setups since clipboard
works without HTTPS there.
Bug 1: detect_tailscale() looked for DNSName at top level of JSON but
tailscale status --self --json nests it inside Self.DNSName. Fixed to
read from Self first with top-level fallback.
Bug 2: getattr(cert, 'not_valid_after_utc', cert.not_valid_after)
eagerly evaluated the fallback, triggering CryptographyDeprecationWarning.
Replaced with try/except AttributeError pattern in all three occurrences
(generate_self_signed and get_cert_info).
muxplex/tls.py imports cryptography for self-signed cert generation
but it was missing from the declared dependencies. uv tool installs
would fail with ModuleNotFoundError on setup-tls.
- test_readme_tls_cert_has_empty_default: docstring now accurately describes
the assertion (key presence check, not empty-string default verification)
- test_readme_tls_key_has_empty_default: same correction
- test_readme_tls_detection_priority_explanation: replace broad three-way or
(detection/priority/auto-detect) with tighter check requiring both 'Tailscale'
and 'mkcert' to appear alongside priority language, so incidental word matches
can no longer satisfy the assertion
Verifies that generate_mkcert() correctly passes Tailscale hostnames
(spark-1.tail8f3c4e.ts.net, 100.64.0.1) through to the mkcert command
arguments when extra_hostnames is provided.
- fake_run tracks gen_calls only for '-cert-file' invocations
- Creates fake cert/key files to simulate mkcert success
- Asserts result is not None
- Asserts Tailscale hostname appears in mkcert command args
Part of Phase 2 full test sweep (task-7).
- Added existing cert detection at the top of setup_tls() in muxplex/cli.py
- After defining cert_path/key_path, checks if tls_cert and tls_key are both set in settings AND the cert file exists
- If so, calls get_cert_info() and prints 'TLS already configured (expires YYYY-MM-DD).'
- Prompts user with input('Regenerate? [y/N] ') with try/except for EOFError and KeyboardInterrupt (defaults to 'n')
- If user does not answer 'y' or 'yes', prints 'Keeping existing certificates.' and returns early
- Added two new tests in test_cli.py: test_setup_tls_prompts_when_certs_exist and test_setup_tls_regenerates_on_eof
- All 866 tests pass
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Added setup_tls_status() function to display current TLS configuration status
- Shows cert path, key path, hostnames, expiry, and status when configured
- Shows 'not configured' prompt when TLS is not set up
- Shows 'configured but cert not readable' when cert exists in settings but file isn't accessible
- Added --status flag to setup-tls argparse subparser
- Updated main() dispatch to call setup_tls_status() when --status flag is passed
- Added 4 new tests covering all scenarios
All 95 tests pass.
Rewrite setup_tls() with full auto-detection chain:
1. Tailscale: detect via detect_tailscale(), generate cert with generate_tailscale()
2. mkcert: detect via detect_mkcert(), generate cert with generate_mkcert()
- Includes optional Tailscale SANs as extra_hostnames when available
3. Self-signed: fallback via generate_self_signed()
For forced methods (--method tailscale/mkcert), exits with code 1 on failure.
Prints cert info (Certificate, Key, Hostnames, Expires) on success.
Method-specific warnings: self-signed (browser warning), tailscale (90-day expiry).
Always prints 'Restart service to apply: muxplex service restart'.
Update argparse --method choices to include 'tailscale' and 'mkcert'.
Add 4 tests:
- test_setup_tls_auto_uses_tailscale_when_available
- test_setup_tls_auto_falls_to_mkcert_when_no_tailscale
- test_setup_tls_auto_falls_to_selfsigned_when_nothing_available
- test_setup_tls_method_choices_expanded
Co-authored-by: Amplifier <amplifier@bkrabach.com>
- Probe Tailscale CLI via shutil.which('tailscale')
- Run 'tailscale status --self --json' with 10-second timeout
- Return dict(hostname, ips, cert_domains) on success
- Return None if not installed, non-zero exit, timeout/JSON/OS error,
empty CertDomains, or empty DNSName
- All imports (json, shutil, subprocess) inside function body
- 4 tests: available, not installed, not connected, no cert domains
- Update test_tls.py module docstring: '9 tests' → '12 tests' (stale after edge case additions)
- Parenthesize assertion message on line 177 of test_tls.py (ruff line-length format)
- Rename test_serve_warns_when_only_cert_set → test_serve_no_ssl_when_only_cert_set
to accurately reflect test behavior (verifies no SSL, not that a warning is printed)
- Inserts TLS check between Serve config and Auth status sections
- Shows enabled status with cert expiry date when valid certs configured
- Shows warning with days-expired count when cert is expired
- Shows configured-but-not-readable warning when cert file unreadable
- Shows disabled warning mentioning clipboard/HTTPS requirement when no TLS
- Adds 3 new tests: tls_disabled, tls_enabled, tls_clipboard_warning
- All 86 CLI tests pass
Co-authored-by: Amplifier <amplifier@amplified.dev>