Skip to content

fix(pairing): stop routing mobile pairing to a non-existent /pair sidecar - #5134

Closed
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:fix/pairing-404-no-pair-sidecar
Closed

fix(pairing): stop routing mobile pairing to a non-existent /pair sidecar#5134
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:fix/pairing-404-no-pair-sidecar

Conversation

@mfethe1

@mfethe1 mfethe1 commented Aug 7, 2026

Copy link
Copy Markdown

What

probe_pairing_relay in desktop/src-tauri/src/commands/pairing.rs fabricated a pairing-relay URL by appending /pair to the main relay's URL whenever NIP-11 advertised supported_nips: [43]. This replaces that fallback (PairingRelay::LegacyPath) with PairingRelay::MembershipWithoutPairingRelay, which returns an actionable error naming the operator fix instead of a URL that 404s.

Why

Pairing a mobile device against wss://flint.communities.buzz.xyz/community fails with:

WebSocket connection failed: HTTP error: 404 Not Found

Two separate defects produce that 404.

1. The fabricated URL is path-scoped; the convention is not. The old code took the relay's existing path and appended /pair (pairing.rs:715-722 on main), so a community relay served under /community produced wss://…/community/pair. But /pair is a reverse-proxy convention for the standalone buzz-pair-relay sidecar, and it is documented as root-only — "Routes only /pair to this sidecar" (crates/buzz-pair-relay/src/lib.rs:11). Nothing routes /community/pair. The test being replaced asserted that wrong URL verbatim (desktop/src-tauri/src/commands/pairing_relay_tests.rs:92 on main).

2. NIP-43 is the wrong signal. NIP-43 means the relay enforces membership; it says nothing about whether a pairing sidecar is deployed. crates/buzz-relay/src/nip11.rs:483 already carries a regression test warning about exactly this misinference.

The other route is genuinely closed on such a relay, so falling back to PairingRelay::MainRelay is not an option either: crates/buzz-relay/src/handlers/event.rs:637-652 rejects any EVENT from an unauthenticated connection with no exemption for ephemeral kinds, and crates/buzz-relay/src/handlers/auth.rs:217-237 runs enforce_relay_membership on every NIP-42 AUTH. An unpaired device cannot publish kind:24134 over a membership relay's root socket at all.

Behaviour change

Relay NIP-11 Before After
pairing_relay_url advertised use it as-is unchanged
No NIP-43 pair over the root socket unchanged
NIP-43, no pairing_relay_url wss://host<path>/pair → 404 error naming the operator fix

This is a breaking change for one deployment shape, and I want that on the record. A membership relay served at the root path, behind a proxy following the same-host convention, got a working wss://host/pair before and gets a hard error now. Migration is a single setting — BUZZ_PAIRING_RELAY_URL=wss://host/pair, or Helm pairingRelay.url — but it is a break, and it makes two in-tree docs stale: deploy/charts/buzz/values.yaml:212-213 ("the legacy same-host /pair convention") and crates/buzz-pair-relay/src/lib.rs:11. I left both untouched pending the decision below.

Verification

  • cargo test --manifest-path desktop/src-tauri/Cargo.toml pairing_relay — 7 tests, all in desktop/src-tauri/src/commands/pairing_relay_tests.rs. One of them, live_nip11_probe_discovers_configured_pairing_relay, binds a local socket and is not #[ignore]d.
  • The new membership_without_pairing_relay_errors_instead_of_guessing_a_path asserts the error never contains /pair and does name the missing pairing relay, so the fabrication cannot silently return.
  • The error text reaches the UI verbatim: pairingErrorMessage (desktop/src/features/settings/ui/MobilePairingCard.tsx:37-50) passes any non-timeout message straight through into the error step. That pass-through is the entire value of the change.
  • No repo CI workflow has run on this PR — the build/test/clippy runs sit at action_required pending maintainer approval, and DCO/Semgrep/zizmor are the only checks that executed. There is no green build to point at.

Scope

  • Desktop client only — 2 files, +37/−20. No relay change, no new event kind, no schema or NIP-11 field.
  • desktop/src-tauri/src/commands/pairing.rs goes 793 → 800 lines, well under the 1000-line ratchet (desktop/scripts/check-file-sizes.mjs).
  • No mobile or web mirror needs updating: pairing_relay_url discovery lives only in this one desktop module. git grep -n -i pairing_relay_url returns 33 hits, all in buzz-relay config/nip11, the Helm chart, and desktop/src-tauri/src/commands/pairing{,_relay_tests}.rs. Mobile receives the already-resolved URL inside the QR payload and never probes NIP-11 itself.
  • Docs that still describe /pair as a client-supported convention are deliberately left alone pending the first open question.

Open questions for maintainers

  1. Retire /pair outright, or keep it for root-path relays? This PR removes it unconditionally. Gating the fallback on url.path() == "/" would preserve every deployment the convention ever actually worked for, at the cost of leaving one guess in the client. If you prefer the unconditional removal, I'll fold the two doc updates into this PR.
  2. Should the relay instead exempt pairing kind:24134 from the membership gate? That would let a single relay serve pairing with no sidecar and remove the separate deployment entirely — but it changes the auth gate and wants a security review, so it is deliberately not attempted here.

@mfethe1
mfethe1 force-pushed the fix/pairing-404-no-pair-sidecar branch from 2385198 to 45a7993 Compare August 17, 2026 11:11
@mfethe1
mfethe1 marked this pull request as ready for review August 17, 2026 11:11
@mfethe1
mfethe1 requested a review from a team as a code owner August 17, 2026 11:11
…ecar

Pairing from a membership-enforcing relay failed with

    WebSocket connection failed: HTTP error: 404 Not Found

`probe_pairing_relay` treated a NIP-11 `supported_nips: [43]` advertisement
as "this relay has a /pair sidecar" and synthesised `wss://host/…/pair`.
No relay serves that path — `buzz-relay` exposes its WebSocket only at `/`
(crates/buzz-relay/src/router.rs), and the real dedicated pairing relay
(`buzz-pair-relay`) is a separate service advertised via NIP-11
`pairing_relay_url`. The `/pair` convention is dead infrastructure, and
NIP-43 was the wrong signal for it: NIP-43 means membership enforcement,
not the presence of a pairing sidecar.

Worse, the fallback could never have worked. On a membership-enforcing
relay an unpaired peer is rejected during NIP-42 AUTH
(crates/buzz-relay/src/handlers/auth.rs, via `enforce_relay_membership` in
crates/buzz-relay/src/api/mod.rs) and there is no exemption for pairing
kind:24134 or for ephemeral events. Such a relay genuinely requires a
separate open pairing relay; it cannot pair over its own root socket.

Replace `LegacyPath` with `MembershipWithoutPairingRelay`, which surfaces
an actionable error naming the operator fix instead of fabricating a URL
that 404s. The two paths that actually work are unchanged: an advertised
`pairing_relay_url` is used as-is, and an open relay pairs over its root
socket.

This is a client-side correctness fix. Operators of membership-enforcing
relays must still deploy a pairing relay (BUZZ_PAIRING_RELAY_URL, or Helm
`pairingRelay.enabled` + `pairingRelay.url`) for pairing to succeed — the
error now says so instead of reporting a bare 404.

Signed-off-by: Michael Feth <michael@jira-flow.com>
@mfethe1
mfethe1 force-pushed the fix/pairing-404-no-pair-sidecar branch from 45a7993 to 22ef8cf Compare August 18, 2026 03:41
@mfethe1

mfethe1 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Closing in favour of #6183, which fixes the same defect and goes further.

Both PRs found the same root cause — probe_pairing_relay treated a NIP-11 supported_nips: [43] advertisement as proof that a same-host /pair WebSocket endpoint exists, and synthesised a URL that 404s. NIP-43 means membership enforcement, not the presence of a pairing sidecar.

#6183 is a strict superset:

  • It touches the same two files (desktop/src-tauri/src/commands/pairing.rs, pairing_relay_tests.rs) and I confirmed the two branches conflict on both — same lines, same fix.
  • It removes the PairingRelay::LegacyPath variant outright, where this PR only renamed it to MembershipWithoutPairingRelay. Removing it is the better end state: the variant cannot be reached for a reason that no longer exists.
  • It also covers what this PR explicitly could not. This PR's description conceded that operators of membership-enforcing relays "must still deploy a pairing relay" for pairing to succeed, and left that to them. fix(pairing): require explicit pairing relay discovery #6183 adds the relay-side NIP-11 advertisement (crates/buzz-relay/src/nip11.rs) plus the deployment wiring (Compose, Caddy, nginx, Helm values) so the fix is actually operable end to end.

Leaving both open costs a reviewer the work of discovering the overlap themselves and then deciding which to take. There is nothing in this PR that #6183 does not already do better, so this one goes.

No action needed on #6183 — flagging the relationship here only so the history is legible.

@mfethe1 mfethe1 closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant