Relay connect used to serialize a dead LAN probe (up to 8s per stale address
on mobile, 2-4s on desktop) in front of the relay attempt, then paid a second
WebSocket connect + E2EE handshake because the probe tunnel was thrown away.
- mobile probeConnectionCandidates: race the relay probe against the direct
chain with a 1.5s direct headstart; a live LAN still wins, a dead one no
longer delays startup
- relay probes adopt their tunnel as the runtime tunnel (adoptRelayTunnel)
instead of dialing a fresh one — applies to auto-connect, pairing redeem,
password login, and the desktop host switcher's relay fallback
- relay probe drops the /health round-trip: the E2EE handshake already proves
the server identity, /auth/session alone proves liveness and auth
- desktop restoreDesktopRelayRuntime: same headstart race; a late direct
success hot-switches back (stable runtimeKey); startup probe now passes
expectedServerId so a re-leased LAN address never sees the token
- launch splash shows 'Connecting to device: <label>' with animated dots
under the (still centered) logo, translated in all locales
- editing a saved instance no longer rebuilds it from the URL field alone:
the id is passed through, relay/https candidates are preserved, and a
token-key change migrates the Keychain token instead of orphaning it
Candidates refresh (server + mobile + desktop clients):
- GET /api/client-auth/connection/candidates returns the server's current
LAN URLs, relay candidate, and serverId for already-paired devices
- /health and /api/version expose serverId so clients can verify a learned
address belongs to the expected server before sending their bearer token
- mobile: refresh saved candidates over the live transport after every
connect/wake, hot-switch relay->LAN when a fresh address is reachable;
serverId gate on direct probes; token no longer sent to /health
- desktop: refresh stored host apiUrl after a relay connect and hot-switch
back to direct; electron probe verifies serverId before authenticated fetch
Fixes found while debugging a dead pairing:
- settings: strict reader that throws on corrupt/unreadable file instead of
returning {}; relay signing/encryption key generation is now gated on it,
so a swallowed read failure can no longer mint a new server identity and
orphan every paired device (loud log when a keypair IS generated)
- SessionAuthGate: bounded auto-retry for transient session-check failures
(initial request racing the relay tunnel's first WS attempt, startup 5xx)
- A saved host now keeps every transport its pairing link carried: direct URL
plus the relay descriptor, with one token for both (the mobile connection
model). Switching tries the direct leg and falls back to the E2EE tunnel;
list probes report Connected · Relay when only the tunnel reaches the host;
relaunch restore picks direct first
- Host switching trusts the dropdown's fresh probe instead of re-probing on
click (no doubled latency, no transient Unreachable flashes); statuses are
written once with the final outcome, survive the dropdown closing via a
last-known cache, and an unprobed host reads Checking — never Unknown
- Open-in-new-window works for relay hosts: a new IPC command boots the local
UI with the host id injected and the renderer picks the transport; the app
render holds on the relay restore so the splash shows instead of a transient
auth screen (10s safety valve)
- Relay host control socket gained protocol-level keepalive: a missed pong
window terminates and reconnects, so the relay can no longer hold a ghost
registration that leaves every client tunnel hanging; the desktop relay
probe also hard-times-out at 8s instead of hanging status flows
- Services dropdown restyled with mobile-style cards: per-provider usage
cards, per-host instance cards with a selected highlight and a toned
status line, MCP servers grouped in a card
Reworks how devices connect to an OpenChamber server, end to end.
Pairing v2:
- One-time pairing links/QR codes (openchamber://connect?v=2) carrying a set of transport candidates (LAN/tunnel/relay) and a single-use secret redeemed server-side; no tokens embedded in links
- Add-a-device dialog written for first-time users: intent-based transport choice (Anywhere / Home network only / This computer only) with plain-language descriptions, transparent fallback checkboxes, server-authoritative LAN detection, high-res QR dialog
- Private relay folded into pairing as a transport candidate with a demand-driven lifecycle (enables when a relay device is paired, disables when none remain)
Multi-transport devices:
- A saved device holds all its transports and one token; mobile re-probes on connect, resume, and network change and hot-switches LAN<->relay seamlessly (no re-pairing, no remount, session preserved)
- Desktop can import relay pairing links, switch to relay hosts through the E2EE tunnel, and restore a relay default host after relaunch
Device management:
- Device list (web + desktop) shows live per-device connectivity with the active transport (Connected - Local network / Relay) and platform badges (iOS/Android/macOS/Windows/Linux)
- One physical device = one record: stable per-install dedupe keys across pairing and password re-login; typed pairing label names the device, paired devices name the connection by the issuing server hostname
- Trusted desktop-local client manages all devices (list, revoke, clear revoked); relay host reaps dead client sockets after 3 missed keepalives
Android:
- LAN transport unblocked (cleartext + mixed content, mirroring iOS ATS exceptions); resume re-probe retries through network flux and silently auto-reconnects from a disconnected state