Legacy /pair pairing fallback cannot pass NIP-42 — desktop signs the URL with path, relay expects bare origin
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
On relays whose NIP-11 lacks `pairing_relay_url` (which includes both tagged relay releases, `relay-v0.1.1` and `relay-v0.2.0`), Buzz Desktop's device pairing falls back to the legacy convention `wss:///pair` — and then cannot authenticate, because the desktop signs its NIP-42 AUTH event with that URL **including the path**, while the relay expects the **bare origin**. The mismatch is unconditional, so legacy-path pairing fails for every operator on the tagged releases.
## The two sides of the contract
**Desktop** (`desktop/src-tauri/src/commands/pairing.rs`): when NIP-11 has no `pairing_relay_url` but advertises NIP-43, `resolve_pairing_relay_url` builds `/pair` (`PairingRelay::LegacyPath`), and `handle_nip42_auth` signs the AUTH event with that full URL:
```rust
let relay_url_parsed = nostr::RelayUrl::parse(relay_url)...; // wss:///pair
... s.sign_event(nostr::EventBuilder::auth(challenge, relay_url_parsed))
```
**Relay** (`crates/buzz-relay/src/api/bridge.rs`, `nip42_expected_relay_url`): the expected value is derived from the tenant host, explicitly pathless — the comment states the contract the desktop is violating:
```rust
/// Path is empty — clients put the bare WS origin (`ws://host[:port]`) in the
/// `relay` tag, matching how `EventBuilder::auth` accepts a [`nostr::RelayUrl`].
format!("{scheme}://{}", tenant.host())
```
`normalize_relay_url` in `crates/buzz-auth/src/nip42.rs` preserves paths (it only trims a trailing slash), so `wss:///pair` vs `wss://` never match. Since every WS connection must complete NIP-42 within the fixed 5s window, the connection is then closed.
## Observed behaviour
Desktop v0.5.4 against `relay-v0.2.0`, with an open pairing relay served behind a path-stripping proxy at `/pair` (so the transport itself works — the WS upgrade succeeds and the QR renders briefly):
Relay log, every attempt:
```
WARN NIP-42 auth failed error="relay url mismatch"
WARN NIP-42 auth timeout — closing connection timeout_secs=5
INFO WebSocket connection closed
```
Desktop surfaces this as `relay closed waiting for EOSE` a few seconds after the QR appears.
## Suggested fix (either side resolves it)
- **Desktop:** sign the bare origin of the pairing URL in the AUTH event — strip the path from `relay_url` before `EventBuilder::auth` in `handle_nip42_auth`. One line, and it matches the relay's documented expectation.
- **Or relay:** make `nip42_expected_relay_url` (or the comparison) tolerant of the client's connect path.
## Operator workaround meanwhile
Run a relay recent enough to support `BUZZ_PAIRING_RELAY_URL`, and advertise the open pairing relay at a **dedicated hostname** (path `/`). The desktop then takes the `PairingRelay::Configured` route, dials a pathless URL, signs the bare origin, and auth succeeds. Verified working end-to-end; only the legacy path convention is broken.
Happy to provide fuller logs or test against a patched build.
Contributor guide
Assessment
This issue has not been assessed yet.