Skip to content

fix(relay): keep the NIP-11 document readable under a CORS allowlist - #6195

Open
guptasiddharth wants to merge 2 commits into
block:mainfrom
guptasiddharth:fix/nip11-cors-permissive
Open

fix(relay): keep the NIP-11 document readable under a CORS allowlist#6195
guptasiddharth wants to merge 2 commits into
block:mainfrom
guptasiddharth:fix/nip11-cors-permissive

Conversation

@guptasiddharth

@guptasiddharth guptasiddharth commented Aug 18, 2026

Copy link
Copy Markdown

Fixes #5550.

Problem

NIP-11 makes cross-origin readability of the relay information document a MUST:

Relays MUST accept CORS requests by sending Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods headers.

build_cors_layer is applied to the whole merged router (router.rs:200), so when BUZZ_CORS_ORIGINS is set the allowlist that (correctly) protects the authenticated REST surface also narrows the relay information document — GET / with Accept: application/nostr+json and GET /info. Browser-based clients hosted anywhere else can no longer read it.

This is on the documented path, not an exotic config: deploy/compose/.env.example ships BUZZ_CORS_ORIGINS=https://buzz.example.com, so following the deployment guide produces a relay that fails the MUST. The response is 200 either way and only the header differs, so it fails silently from the server's perspective.

Fix

Serve the document with permissive CORS headers set on the response (relay_info_response in nip11.rs), used by both the /info handler and the content-negotiated / branch.

I did not split the document into its own permissively-layered Router as the issue suggested, because / is content-negotiated with the WebSocket upgrade — the NIP-11 branch is selected by Accept, not by path, so it cannot be routed away from the WS handler. Setting the headers on the response covers both entry points and composes correctly with the existing layer: tower_http's CORS layer overwrites only the header names it emits, so

  • a non-allowlisted origin keeps the permissive value set here, and
  • an allowlisted origin still gets its own value echoed back — exactly one Access-Control-Allow-Origin, never two.

This is the same pattern the NIP-05 document already uses (api/nip05.rs), so the two public documents now behave alike.

Nothing else is widened: BUZZ_CORS_ORIGINS keeps governing /events, /query, /count, the media endpoints and the rest of the surface. The document carries no private data — capabilities, limits, and the relay's public key — and auth_required/restricted_writes continue to gate everything that matters.

Tests

Three tests in router::tests, driving the real build_cors_layer with a configured allowlist:

  • nip11_document_stays_readable_from_any_origin_under_a_cors_allowlist — a foreign origin gets all three headers on /info. Fails on main (left: [], right: ["*"]), which is the reported symptom.
  • allowlisted_origin_gets_exactly_one_allow_origin_on_the_nip11_document — guards the duplicate-header hazard.
  • cors_allowlist_still_narrows_the_authenticated_surface — a bridge route still gets no Access-Control-Allow-Origin for a foreign origin, so the exemption cannot silently widen.

Second commit: making those tests actually run

While checking which job would execute them, I found that nothing does. just test-unit enumerates packages explicitly (because nothing in CI runs cargo test --workspace) and buzz-relay is not on the list; the backend integration job archives the package's lib tests but only ever selects api::invites::tests and handlers::relay_admin::tests. So router::tests and nip11::tests are compiled by clippy and the archive build, then executed by nothing — a regression test that never runs would not have guarded this fix.

The second commit selects those two modules in the unit job, where they belong: both are pure in-process axum + tower coverage needing no Postgres or Redis (23 tests, verified passing with no services up). The rest of the package's lib set is infra-backed and stays where it is. Mirrored into scripts/run-tests.sh, the recipe's fallback when cargo-nextest is absent, since the surrounding comments there ask for the two lists to stay in step.

I put this in just test-unit rather than adding a step to ci.yml on purpose: these tests need no infra, and .github/workflows/ci.yml is in the hashFiles key for the relay-artifacts cache, so editing it would invalidate that cache for everyone. Happy to move it if you'd rather have it elsewhere — or to drop the commit entirely if the coverage gap is deliberate.

cargo test -p buzz-relay --lib router::tests    # 8 passed
cargo test -p buzz-relay --lib                  # 886 passed; the 8 failures (api::admin, api::media)
                                                # are pre-existing on a clean tree here — they need Postgres/Redis
cargo clippy -p buzz-relay --all-targets -- -D warnings   # clean (the `just clippy` gate)
cargo fmt --all -- --check                      # clean (the `just fmt-check` gate)
cargo nextest run -p buzz-relay --lib -E 'test(/^router::tests/) or test(/^nip11::tests/)'
                                                # 23 passed, no infra — the new unit-job selection

Manual check against the issue's repro, with BUZZ_CORS_ORIGINS=https://relay.example.com set:

$ curl -sSI -H 'Origin: https://some-web-client.example' -H 'Accept: application/nostr+json' <relay>/ | grep -i access-control
access-control-allow-origin: *
access-control-allow-headers: *
access-control-allow-methods: *

Related

Searched open PRs before starting — no PR referenced #5550. Closest in the area: none touching build_cors_layer or nip11.rs.

One adjacent observation, not changed here: NIP-05 requires Access-Control-Allow-Origin: * on /.well-known/nostr.json and the handler already sets it, so that one is conformant today — it just gets there by the same mechanism this PR now uses for NIP-11.

NIP-11 requires the relay information document to be readable cross-origin.
`build_cors_layer` wraps every route, so setting `BUZZ_CORS_ORIGINS` — which
`deploy/compose/.env.example` tells operators to do — also narrows `GET /`
with `Accept: application/nostr+json` and `GET /info` to the allowlist, and
browser clients hosted anywhere else can no longer read the document. The
request still returns 200, so the failure is invisible server-side.

Serve the document with permissive CORS headers on the response instead of
splitting the route: `/` is content-negotiated with the WebSocket upgrade and
cannot be split off, and tower_http's CORS layer overwrites only the header
names it emits — so an allowlisted origin still gets its own value echoed back
(exactly one), while every other origin keeps the permissive one. This is the
pattern the NIP-05 document already uses. The allowlist keeps governing
/events, /query, /count, and the media surface.

Fixes block#5550

Signed-off-by: guptasiddharth <guptasiddharth12@gmail.com>
@guptasiddharth
guptasiddharth requested a review from a team as a code owner August 18, 2026 07:10
`just test-unit` enumerates packages explicitly because nothing in CI runs
`cargo test --workspace`, and buzz-relay is not on that list. The backend
integration job archives the package's lib tests (`cargo nextest archive -p
buzz-relay --lib`) but only ever selects `api::invites::tests` and
`handlers::relay_admin::tests`, so `router::tests` and `nip11::tests` are
compiled by clippy and the archive build, then executed by nothing.

Select those two modules in the unit job, where they belong: both are pure
in-process axum + tower coverage with no Postgres or Redis. The rest of the
package's lib set is infra-backed and stays where it is. Mirrored into
scripts/run-tests.sh, which the recipe falls back to when cargo-nextest is
absent — the two lists must stay in step, as the surrounding comments note.

Verified: 23 tests run, 23 passed, no services running.
Signed-off-by: guptasiddharth <guptasiddharth12@gmail.com>
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.

NIP-11 relay information document is not CORS-accessible when BUZZ_CORS_ORIGINS is set

1 participant