mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
fix: authenticate to NIP-29 host relays of joined groups so private group content loads
A private/closed NIP-29 group serves its content (kind-9 chat, kind-11 threads, …) only over a NIP-42-authenticated connection. Amethyst's group-content reads are all `#h`-scoped with all authors, so they never name the user's pubkey, and a group host relay (e.g. wss://chat.wisp.talk) is not in any of the NIP-65/DM/search/… lists that feed `account.trustedRelays`. The per-account first-party AUTH gate therefore returned false and Amethyst never AUTHed, so the relay refused the content with `auth-required` — the group's public 39000 metadata still loaded, so the group appeared but showed no messages. Treat a NIP-29 relay group the user explicitly joined (their kind-10009 list) as a first-party reason to authenticate with its host relay, mirroring the existing `BuzzWorkspaces.isJoined` carve-out. Reads of a group the user has not joined (e.g. browsing a relay's public directory) still do not AUTH. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018puT2YnevbLHYG2PdCLAeh
This commit is contained in:
+7
@@ -221,6 +221,13 @@ class AuthCoordinator(
|
||||
relayUrl = relayUrl,
|
||||
pendingEvents = client.activeOutboxEvents(relayUrl),
|
||||
myRelays = account.trustedRelays.flow.value,
|
||||
// A NIP-29 relay group the user explicitly joined (kind-10009) is a first-party reason
|
||||
// to authenticate with its host relay: private/closed group content is `#h`-scoped and
|
||||
// never names the user, so it fails the pubkey checks above — without this, a joined
|
||||
// private group's messages are refused with `auth-required` and the group stays empty.
|
||||
myGroupRelays =
|
||||
account.relayGroupList.liveRelayGroupIds.value
|
||||
.mapTo(mutableSetOf()) { it.relayUrl },
|
||||
)
|
||||
|
||||
fun destroy() {
|
||||
|
||||
+11
-2
@@ -34,7 +34,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
* - it is publishing its own event there ([pendingEvents] authored by it — e.g. delivering a DM to
|
||||
* the recipient's inbox relay), or
|
||||
* - the relay is one it configured itself ([myRelays] — its NIP-65 / DM / search / … lists, which
|
||||
* is where its own inbox/outbox reads are routed anyway).
|
||||
* is where its own inbox/outbox reads are routed anyway), or
|
||||
* - the relay hosts a NIP-29 relay group the account explicitly joined ([myGroupRelays], from its
|
||||
* kind-10009 list). A private/closed group's content (kind-9 chat, kind-11 threads, …) is
|
||||
* `#h`-scoped and served only to authenticated members; that read never names the user, so it
|
||||
* can't qualify via [pendingEvents], and a group host relay is not one of the account's own
|
||||
* NIP-65/DM/… lists, so it can't qualify via [myRelays] either. Without this, a joined private
|
||||
* group is refused with `auth-required` and renders empty — the group's own metadata (39000) is
|
||||
* public and still loads, so the group appears but shows no messages.
|
||||
*
|
||||
* Crucially, an active subscription merely *naming* the account (a `#p` tag or `authors` entry) is
|
||||
* NOT a first-party reason: the app packs several accounts' pubkeys into one merged filter and fans
|
||||
@@ -49,8 +56,10 @@ object RelayAuthFirstParty {
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
pendingEvents: List<Event>,
|
||||
myRelays: Set<NormalizedRelayUrl>,
|
||||
myGroupRelays: Set<NormalizedRelayUrl> = emptySet(),
|
||||
): Boolean {
|
||||
if (pendingEvents.any { it.pubKey == me }) return true
|
||||
return relayUrl in myRelays
|
||||
if (relayUrl in myRelays) return true
|
||||
return relayUrl in myGroupRelays
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -71,4 +71,21 @@ class RelayAuthFirstPartyTest {
|
||||
// Delivering my own DM/post to the recipient's relay, even one not in my list.
|
||||
assertTrue(RelayAuthFirstParty.hasReason(me, relay, listOf(event(me)), emptySet()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aRelayHostingAJoinedGroupIsFirstParty() {
|
||||
// A NIP-29 group host relay is in none of my NIP-65/DM/… lists, and a private group's content
|
||||
// is `#h`-scoped so it never names me — the joined-group set is the only signal that lets us
|
||||
// AUTH so the relay serves the group's `auth-required` content instead of leaving it empty.
|
||||
val groupRelay = NormalizedRelayUrl("wss://chat.wisp.talk/")
|
||||
assertTrue(RelayAuthFirstParty.hasReason(me, groupRelay, emptyList(), emptySet(), setOf(groupRelay)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aGroupRelayIHaveNotJoinedIsNotFirstParty() {
|
||||
// Merely knowing a group relay exists (e.g. browsing its public directory) must not AUTH it;
|
||||
// only a group on my own kind-10009 list counts.
|
||||
val groupRelay = NormalizedRelayUrl("wss://chat.wisp.talk/")
|
||||
assertFalse(RelayAuthFirstParty.hasReason(me, groupRelay, emptyList(), emptySet(), emptySet()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user