Skip to content

CON-46: ask before connecting to a relay a link picked - #91

Merged
molgerz merged 1 commit into
mainfrom
con-46-relay-trust-prompt
Sep 19, 2026
Merged

molgerz merged 1 commit into
mainfrom
con-46-relay-trust-prompt

Conversation

@molgerz

@molgerz molgerz commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes CON-46. From the CON-43 security audit. Stacked on con-45-group-address-host — merge CON-45 first.

A space carries its relay in its own address (/s/<host>'<group>), so a link decides, on its author's behalf, which host the reader's browser opens a WebSocket to, fetches a NIP-11 document from over HTTPS, and — while signed in — answers a NIP-42 challenge from with an event signed by the reader's key. Following a link is not consent to any of that.

What is in it

  • An unfamiliar host is asked about once, before anything is sent. The screen names the host in monospace and spells out what connecting tells it: the reader's IP and the time, and while signed in an event naming their npub.
  • "Remember this relay" is checked by default and kept per host in localStorage. Unchecked it still connects, for this visit only — asking again on every reload would train the reader to click through.
  • Both the connection and the AUTH happen in effects inside useRelay, so "do not connect" cannot mean calling the hook and ignoring it. The shell is split: Shell decides, SpaceShell holds every relay-consuming hook (including the <Outlet>) and either mounts or does not.
  • An address that does not parse gets its own screen instead of a fall back to DEFAULT_RELAY_URL — until now an unreadable link opened a connection to the deployment's relay purely so the view above could draw "Invalid group address." on top of it.
  • Loopback and the host of VITE_RELAY_URL are trusted by construction: the first through isLocalRelayHost (CON-45, not a second copy of the rule), the second because operator configuration is not link input.
  • /settings/spaces/<host>'<group> stays outside the gate on purpose — that route calls no useRelay, so nothing holds the connection and SpaceStore.start() returns at its readiness check. AppShell.trust.test.tsx pins it, so wiring a relay through the settings route fails a test rather than quietly connecting.

How to test

  1. Clear site data. Open a space link whose host is neither loopback nor VITE_RELAY_URL (e.g. /s/relay.example'engineering). Before clicking anything, check DevTools → Network: no WebSocket, no NIP-11 request to that host.
  2. Click "Connect": the connection is attempted and the space loads (or fails on its own terms).
  3. Reload: no prompt this time.
  4. Repeat with "Remember this relay" unchecked — it connects, and the prompt is back after a reload.
  5. Follow a link to a second unfamiliar host: asked again, and the checkbox is reset rather than inheriting the previous answer.
  6. /s/not-an-address shows the "This link is not a space address" screen, and no relay connection is made at all.
  7. The local dev relay and the deployment's own relay never prompt.
  8. /settings/spaces/<unknown host>'group shows the settings nav without a prompt — and still opens no socket (check Network).

Known gap

Taking an approval back means clearing the site's data. A "Trusted relays" list under /settings/profile is deliberately not in this ticket and is written down as residual risk in docs/09.

A space carries its relay in its own address, so a link decides on its author's
behalf which host the reader's browser opens a WebSocket to, fetches a NIP-11
document from, and — while signed in — answers a NIP-42 challenge from with an
event signed by the reader's key. Following a link is not consent to any of
that. An unfamiliar host is asked about once, before anything is sent, and the
answer is remembered per host in `localStorage`.

Both the connection and the AUTH happen inside effects in `useRelay`, so "do
not connect" cannot mean calling the hook and ignoring it — the hook must not
be called. The shell is split for that: `Shell` decides, `SpaceShell` holds
every relay-consuming hook including the `<Outlet>`, and either mounts or does
not.

Two reasons reach that one mechanism and stay apart as two screens, because a
broken link and an undecided one are different things to be told. An address
that does not parse no longer falls back to `DEFAULT_RELAY_URL`: until now it
connected to the deployment's own relay purely so the view above could draw
"Invalid group address." on top of it.

Loopback and the host of `VITE_RELAY_URL` are trusted by construction — the
first asks `isLocalRelayHost` rather than keeping a second copy of that rule
(CON-45 is what one costs), the second is operator configuration, and prompting
for a reader's own deployment would only teach them to click through.

`/settings/spaces/<host>'<group>` stays outside the gate on purpose: that route
calls no `useRelay`, so nothing holds the connection and `SpaceStore.start()`
returns at its readiness check. `AppShell.trust.test.tsx` pins that, so wiring a
relay through the settings route fails a test rather than quietly connecting.

Revoking a remembered relay still means clearing the site's data; the "Trusted
relays" list under /settings/profile is deliberately not in this ticket and is
written down as residual risk in docs/09.
@molgerz
molgerz changed the base branch from con-45-group-address-host to main September 19, 2026 00:07
@molgerz
molgerz merged commit e367996 into main Sep 19, 2026
1 check passed
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