mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(relayauth): venue-aware auth for public chats, communities & live streams
The purpose model was person-centric, so an auth-required NIP-28 channel,
NIP-72 community, or NIP-53 live stream broke: reads filter by #e/#a with
no authors, so nothing was attributable -> silent DENY (chat wouldn't
load); top-level posts (no p tags) also silently failed; replies were
mis-attributed as "notify" and trusted on the wrong signal.
Add venue purposes (POST_VENUE / READ_VENUE) carrying the venue id
(channel event-id, or community/live `kind:pubkey:dTag` address). The
deriver recognizes kind-42 channel posts (root `e`), community/live posts
and reads (`#a` 34550/30311), and channel reads (`#e`). Venues are trusted
under TRUSTED_FOLLOWS when you've joined them (publicChatList /
communityList) or their owner — the pubkey in the address, e.g. a live
stream's host — is someone you follow; trusted venues auto-auth for both
reading and posting.
Safety net: any active use we still can't attribute yields an OTHER
purpose so the relay is prompted about instead of failing silently.
Prompt copy gains venue-aware titles/consequences ("Post to this room?",
"If you don't, your message won't be posted."). Default policy already
TRUSTED_FOLLOWS, so joined venues just work. Unit-tested across resolver
and deriver.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
This commit is contained in:
+10
@@ -30,6 +30,9 @@ import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.ScreenAu
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
|
||||
|
||||
/** The owner pubkey of an addressable venue (`kind:pubkey:dTag`), or null for a bare channel id. */
|
||||
private fun venueOwnerPubkey(venueId: String): String? = venueId.split(':').getOrNull(1)?.takeIf { it.length == 64 }
|
||||
|
||||
@Composable
|
||||
fun RelayAuthSubscription(accountViewModel: AccountViewModel) = RelayAuthSubscription(accountViewModel, Amethyst.instance.authCoordinator)
|
||||
|
||||
@@ -61,6 +64,13 @@ fun RelayAuthSubscription(
|
||||
// Any follow list (kind 3, follow sets, etc.) counts as trusting the counterparty
|
||||
// enough to reveal our identity to a relay that serves them.
|
||||
isFollowed = { pubkey -> pubkey in account.allFollows.flow.value.authors },
|
||||
// A venue (public chat / community / live stream) is trusted if we've joined it, or
|
||||
// its owner — the pubkey in a `kind:pubkey:dTag` address — is someone we follow.
|
||||
isTrustedVenue = { venueId ->
|
||||
venueId in account.publicChatList.flowSet.value ||
|
||||
venueId in account.communityList.flowSet.value ||
|
||||
venueOwnerPubkey(venueId)?.let { it in account.allFollows.flow.value.authors } == true
|
||||
},
|
||||
readTrustEnabled = { account.settings.relayAuthTrustFollowsForReads.value },
|
||||
)
|
||||
}
|
||||
|
||||
+17
-3
@@ -265,13 +265,23 @@ private fun reasonRes(kind: AuthPurposeKind): Int =
|
||||
AuthPurposeKind.SEND_DM -> R.string.relay_auth_reason_send_dm
|
||||
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_reason_notify_inbox
|
||||
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
|
||||
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_reason_post_venue
|
||||
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_reason_read_venue
|
||||
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
|
||||
AuthPurposeKind.OTHER -> R.string.relay_auth_reason_other
|
||||
}
|
||||
|
||||
/** The purpose whose counterparties best describe what the user was doing (most user-facing first). */
|
||||
/** The purpose that best describes what the user was doing (most user-facing first). */
|
||||
private fun List<AuthPurpose>.primaryNamed(): AuthPurpose? =
|
||||
listOf(AuthPurposeKind.SEND_DM, AuthPurposeKind.NOTIFY_INBOX, AuthPurposeKind.READ_OUTBOX)
|
||||
.firstNotNullOfOrNull { kind -> firstOrNull { it.kind == kind && it.counterparties.isNotEmpty() } }
|
||||
listOf(
|
||||
AuthPurposeKind.SEND_DM,
|
||||
AuthPurposeKind.NOTIFY_INBOX,
|
||||
AuthPurposeKind.POST_VENUE,
|
||||
AuthPurposeKind.READ_OUTBOX,
|
||||
AuthPurposeKind.READ_VENUE,
|
||||
).firstNotNullOfOrNull { kind ->
|
||||
firstOrNull { it.kind == kind && (it.counterparties.isNotEmpty() || it.venues.isNotEmpty()) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun titleFor(
|
||||
@@ -282,6 +292,8 @@ private fun titleFor(
|
||||
AuthPurposeKind.SEND_DM -> stringRes(R.string.relay_auth_title_send_dm, who ?: "")
|
||||
AuthPurposeKind.NOTIFY_INBOX -> stringRes(R.string.relay_auth_title_notify, who ?: "")
|
||||
AuthPurposeKind.READ_OUTBOX -> stringRes(R.string.relay_auth_title_read, who ?: "")
|
||||
AuthPurposeKind.POST_VENUE -> stringRes(R.string.relay_auth_title_post_venue)
|
||||
AuthPurposeKind.READ_VENUE -> stringRes(R.string.relay_auth_title_read_venue)
|
||||
else -> stringRes(R.string.relay_auth_prompt_title)
|
||||
}
|
||||
|
||||
@@ -294,6 +306,8 @@ private fun consequenceFor(
|
||||
AuthPurposeKind.SEND_DM -> stringRes(R.string.relay_auth_consequence_send_dm, who ?: "")
|
||||
AuthPurposeKind.NOTIFY_INBOX -> stringRes(R.string.relay_auth_consequence_notify, who ?: "")
|
||||
AuthPurposeKind.READ_OUTBOX -> stringRes(R.string.relay_auth_consequence_read, who ?: "")
|
||||
AuthPurposeKind.POST_VENUE -> stringRes(R.string.relay_auth_consequence_post_venue)
|
||||
AuthPurposeKind.READ_VENUE -> stringRes(R.string.relay_auth_consequence_read_venue)
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -43,6 +43,7 @@ class RelayAuthPermissionLedger(
|
||||
val isInMyRelayList: (String) -> Boolean = { false },
|
||||
val isBlocked: (String) -> Boolean = { false },
|
||||
val isFollowed: (String) -> Boolean = { false },
|
||||
val isTrustedVenue: (String) -> Boolean = { false },
|
||||
val readTrustEnabled: () -> Boolean = { false },
|
||||
) {
|
||||
/** The authorization verdict for [ctx], taking the challenge's purpose into account. */
|
||||
@@ -62,9 +63,19 @@ class RelayAuthPermissionLedger(
|
||||
ctx.purposes.any { p ->
|
||||
p.kind == AuthPurposeKind.READ_OUTBOX && p.counterparties.any(isFollowed)
|
||||
},
|
||||
servesTrustedVenue =
|
||||
ctx.purposes.any { p ->
|
||||
(p.kind == AuthPurposeKind.POST_VENUE || p.kind == AuthPurposeKind.READ_VENUE) &&
|
||||
p.venues.any(isTrustedVenue)
|
||||
},
|
||||
readTrustEnabled = readTrustEnabled(),
|
||||
hasAttributablePurpose =
|
||||
ctx.purposes.any { it.kind == AuthPurposeKind.MY_OWN_RELAY || it.counterparties.isNotEmpty() },
|
||||
ctx.purposes.any {
|
||||
it.kind == AuthPurposeKind.MY_OWN_RELAY ||
|
||||
it.kind == AuthPurposeKind.OTHER ||
|
||||
it.counterparties.isNotEmpty() ||
|
||||
it.venues.isNotEmpty()
|
||||
},
|
||||
)
|
||||
return RelayAuthResolver.resolve(inputs)
|
||||
}
|
||||
|
||||
+54
-11
@@ -25,17 +25,25 @@ import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
|
||||
/** `a`-address prefixes of venues whose home relay may require auth: NIP-72 communities (34550) and
|
||||
* NIP-53 live activities (30311). Their addresses are `kind:ownerPubkey:dTag`. */
|
||||
private val VENUE_ADDRESS_PREFIXES = listOf("34550:", "30311:")
|
||||
|
||||
/**
|
||||
* Infers *why* a relay wants NIP-42 auth from what Amethyst is currently doing with it — the
|
||||
* events pending delivery and the active subscription filters (both from the [INostrClient]).
|
||||
* Pure so it can be unit-tested without the relay client.
|
||||
*
|
||||
* - a pending gift wrap (kind 1059) => we're sending a DM to its `p` recipient ([AuthPurposeKind.SEND_DM]);
|
||||
* - any other pending event carrying `p` tags => we're delivering it to those users' inboxes
|
||||
* ([AuthPurposeKind.NOTIFY_INBOX]);
|
||||
* - a subscription filter with `authors` => we're reading those authors' posts ([AuthPurposeKind.READ_OUTBOX]).
|
||||
* - a pending gift wrap (kind 1059) => sending a DM to its `p` recipient ([AuthPurposeKind.SEND_DM]);
|
||||
* - a pending channel/community/live post => [AuthPurposeKind.POST_VENUE] for that venue;
|
||||
* - any other pending event with `p` tags => delivering it to those users' inboxes ([AuthPurposeKind.NOTIFY_INBOX]);
|
||||
* - a filter with `authors` => reading those authors' posts ([AuthPurposeKind.READ_OUTBOX]);
|
||||
* - a filter with `#e`/`#a` venue tags => reading a venue ([AuthPurposeKind.READ_VENUE]);
|
||||
* - anything else we're actively doing but can't attribute => [AuthPurposeKind.OTHER], so the relay
|
||||
* is prompted about instead of silently failing.
|
||||
*/
|
||||
object RelayAuthPurposeDeriver {
|
||||
fun derive(
|
||||
@@ -44,26 +52,61 @@ object RelayAuthPurposeDeriver {
|
||||
): List<AuthPurpose> {
|
||||
val dmRecipients = mutableSetOf<HexKey>()
|
||||
val notifyRecipients = mutableSetOf<HexKey>()
|
||||
val postVenues = mutableSetOf<String>()
|
||||
var unattributedWrite = false
|
||||
|
||||
pendingEvents.forEach { event ->
|
||||
val pTags = event.tags.mapNotNullTo(mutableSetOf()) { if (it.size > 1 && it[0] == "p") it[1] else null }
|
||||
if (event.kind == GiftWrapEvent.KIND) {
|
||||
dmRecipients.addAll(pTags)
|
||||
} else {
|
||||
// We're notifying the people the event references, not its author — drop the
|
||||
// author's own key so a self-p-tag doesn't read as "notify yourself".
|
||||
notifyRecipients.addAll(pTags - event.pubKey)
|
||||
val venueAddresses = event.tags.venueAddresses()
|
||||
when {
|
||||
event.kind == GiftWrapEvent.KIND -> dmRecipients.addAll(pTags)
|
||||
event.kind == ChannelMessageEvent.KIND -> event.channelRootId()?.let { postVenues.add(it) }
|
||||
venueAddresses.isNotEmpty() -> postVenues.addAll(venueAddresses)
|
||||
pTags.isNotEmpty() -> notifyRecipients.addAll(pTags - event.pubKey)
|
||||
else -> unattributedWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
val readAuthors = mutableSetOf<HexKey>()
|
||||
val readVenues = mutableSetOf<String>()
|
||||
var unattributedRead = false
|
||||
activeFilters.values.forEach { filters ->
|
||||
filters.forEach { filter -> filter.authors?.let(readAuthors::addAll) }
|
||||
filters.forEach { filter ->
|
||||
var matched = false
|
||||
filter.authors?.let {
|
||||
readAuthors.addAll(it)
|
||||
matched = true
|
||||
}
|
||||
filter.tags?.get("e")?.let {
|
||||
readVenues.addAll(it)
|
||||
matched = true
|
||||
}
|
||||
filter.tags?.get("a")?.filter { addr -> VENUE_ADDRESS_PREFIXES.any(addr::startsWith) }?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
readVenues.addAll(it)
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if (!matched) unattributedRead = true
|
||||
}
|
||||
}
|
||||
|
||||
return buildList {
|
||||
if (dmRecipients.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.SEND_DM, dmRecipients))
|
||||
if (notifyRecipients.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.NOTIFY_INBOX, notifyRecipients))
|
||||
if (postVenues.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.POST_VENUE, venues = postVenues))
|
||||
if (readAuthors.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.READ_OUTBOX, readAuthors))
|
||||
if (readVenues.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.READ_VENUE, venues = readVenues))
|
||||
// Safety net: we're using this relay but couldn't say how — prompt rather than fail silently.
|
||||
if (isEmpty() && (unattributedWrite || unattributedRead)) add(AuthPurpose(AuthPurposeKind.OTHER))
|
||||
}
|
||||
}
|
||||
|
||||
/** The venue a channel message posts into: the `e` tag marked "root", else the first `e` tag. */
|
||||
private fun Event.channelRootId(): HexKey? {
|
||||
val eTags = tags.filter { it.size > 1 && it[0] == "e" }
|
||||
return eTags.firstOrNull { it.size > 3 && it[3] == "root" }?.get(1) ?: eTags.firstOrNull()?.get(1)
|
||||
}
|
||||
|
||||
private fun Array<Array<String>>.venueAddresses(): List<String> = mapNotNull { if (it.size > 1 && it[0] == "a" && VENUE_ADDRESS_PREFIXES.any(it[1]::startsWith)) it[1] else null }
|
||||
}
|
||||
|
||||
+3
@@ -366,7 +366,10 @@ private fun reasonRes(kind: AuthPurposeKind): Int =
|
||||
AuthPurposeKind.SEND_DM -> R.string.relay_auth_reason_send_dm
|
||||
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_reason_notify_inbox
|
||||
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
|
||||
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_reason_post_venue
|
||||
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_reason_read_venue
|
||||
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
|
||||
AuthPurposeKind.OTHER -> R.string.relay_auth_reason_other
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -835,14 +835,21 @@
|
||||
<string name="relay_auth_title_send_dm">Send your message to %1$s?</string>
|
||||
<string name="relay_auth_title_notify">Notify %1$s?</string>
|
||||
<string name="relay_auth_title_read">Load posts from %1$s?</string>
|
||||
<string name="relay_auth_title_post_venue">Post to this room?</string>
|
||||
<string name="relay_auth_title_read_venue">Open this room?</string>
|
||||
<!-- What happens if the user doesn\'t confirm, tied to what they were doing. %1$s is the person. -->
|
||||
<string name="relay_auth_consequence_send_dm">If you don\'t, your message to %1$s won\'t be delivered.</string>
|
||||
<string name="relay_auth_consequence_notify">If you don\'t, %1$s won\'t be notified about this.</string>
|
||||
<string name="relay_auth_consequence_read">If you don\'t, you won\'t see posts from %1$s here.</string>
|
||||
<string name="relay_auth_consequence_post_venue">If you don\'t, your message won\'t be posted.</string>
|
||||
<string name="relay_auth_consequence_read_venue">If you don\'t, you won\'t see it here.</string>
|
||||
<string name="relay_auth_name_and_others">%1$s and others</string>
|
||||
<string name="relay_auth_reason_send_dm">Send your private message to:</string>
|
||||
<string name="relay_auth_reason_notify_inbox">Notify:</string>
|
||||
<string name="relay_auth_reason_read_outbox">Download posts from:</string>
|
||||
<string name="relay_auth_reason_post_venue">Post to this room</string>
|
||||
<string name="relay_auth_reason_read_venue">Open this room</string>
|
||||
<string name="relay_auth_reason_other">Use this relay</string>
|
||||
<string name="relay_auth_reason_my_own_relay">Connect to your own relay</string>
|
||||
<string name="relay_auth_and_others">…and others</string>
|
||||
<string name="relay_auth_allow_once">Allow once</string>
|
||||
|
||||
+41
-3
@@ -97,9 +97,47 @@ class RelayAuthPurposeDeriverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noAttributableWorkYieldsNoPurposes() {
|
||||
fun noActivityYieldsNoPurposes() {
|
||||
assertEquals(emptyList<AuthPurpose>(), RelayAuthPurposeDeriver.derive(emptyList(), emptyMap()))
|
||||
// an event with no p tags gives nothing to attribute a notification to
|
||||
assertEquals(emptyList<AuthPurpose>(), RelayAuthPurposeDeriver.derive(listOf(event(1)), emptyMap()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unattributableActivityYieldsOtherAsSafetyNet() {
|
||||
// An event we can't attribute (no p tags, not a venue) still prompts rather than fail silently.
|
||||
val purposes = RelayAuthPurposeDeriver.derive(listOf(event(1)), emptyMap())
|
||||
assertEquals(listOf(AuthPurposeKind.OTHER), purposes.map { it.kind })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun channelMessageBecomesPostVenue() {
|
||||
val channelId = "e".repeat(64)
|
||||
val ev =
|
||||
Event(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "11".repeat(32),
|
||||
createdAt = 1_700_000_000L,
|
||||
kind = 42,
|
||||
tags = arrayOf(arrayOf("e", channelId, "", "root")),
|
||||
content = "hi",
|
||||
sig = "22".repeat(64),
|
||||
)
|
||||
val purposes = RelayAuthPurposeDeriver.derive(listOf(ev), emptyMap())
|
||||
assertEquals(1, purposes.size)
|
||||
assertEquals(AuthPurposeKind.POST_VENUE, purposes[0].kind)
|
||||
assertEquals(setOf(channelId), purposes[0].venues)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun communityAndLiveSubscriptionsBecomeReadVenue() {
|
||||
val community = "34550:${"1".repeat(64)}:my-community"
|
||||
val live = "30311:${"2".repeat(64)}:my-stream"
|
||||
val purposes =
|
||||
RelayAuthPurposeDeriver.derive(
|
||||
emptyList(),
|
||||
mapOf("sub" to listOf(Filter(tags = mapOf("a" to listOf(community, live))))),
|
||||
)
|
||||
assertEquals(1, purposes.size)
|
||||
assertEquals(AuthPurposeKind.READ_VENUE, purposes[0].kind)
|
||||
assertEquals(setOf(community, live), purposes[0].venues)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -38,17 +38,29 @@ enum class AuthPurposeKind {
|
||||
/** Reading an author's posts from their NIP-65 outbox (write relays). */
|
||||
READ_OUTBOX,
|
||||
|
||||
/** Posting into a venue — a NIP-28 public chat or a NIP-72 community — hosted on this relay. */
|
||||
POST_VENUE,
|
||||
|
||||
/** Reading a venue's content (public chat / community) from this relay. */
|
||||
READ_VENUE,
|
||||
|
||||
/** The relay is in the user's own relay list. */
|
||||
MY_OWN_RELAY,
|
||||
|
||||
/** We're actively using this relay but couldn't attribute a specific purpose (safety net so we
|
||||
* prompt instead of silently failing). */
|
||||
OTHER,
|
||||
}
|
||||
|
||||
/**
|
||||
* A single reason a relay connection needs auth, with the counterparties it concerns.
|
||||
* [counterparties] is empty for [AuthPurposeKind.MY_OWN_RELAY].
|
||||
* A single reason a relay connection needs auth. [counterparties] holds the people it concerns
|
||||
* (pubkeys, for DM/notify/outbox purposes); [venues] holds venue identifiers (channel event-ids or
|
||||
* community `a`-addresses, for [AuthPurposeKind.POST_VENUE]/[AuthPurposeKind.READ_VENUE]).
|
||||
*/
|
||||
data class AuthPurpose(
|
||||
val kind: AuthPurposeKind,
|
||||
val counterparties: Set<String> = emptySet(),
|
||||
val venues: Set<String> = emptySet(),
|
||||
)
|
||||
|
||||
/** The relay plus every live reason we currently have to auth with it. */
|
||||
|
||||
+7
-3
@@ -33,6 +33,8 @@ package com.vitorpamplona.amethyst.commons.relayauth
|
||||
* (send DM / deliver notification) for this relay.
|
||||
* @param servesFollowedReadCounterparty a followed user is a counterparty of a *read* purpose
|
||||
* (download their outbox) for this relay.
|
||||
* @param servesTrustedVenue this relay hosts a venue (public chat, community, or live stream) the
|
||||
* user has joined, or whose owner they follow. Trusts both reading and posting to it.
|
||||
* @param readTrustEnabled the "also trust follows' outboxes when reading" sub-toggle.
|
||||
* @param hasAttributablePurpose we know *why* this relay wants auth (so a prompt can explain it).
|
||||
* When false, an unresolved challenge is denied silently rather than prompting.
|
||||
@@ -44,6 +46,7 @@ data class RelayAuthInputs(
|
||||
val isInMyRelayList: Boolean,
|
||||
val servesFollowedWriteCounterparty: Boolean,
|
||||
val servesFollowedReadCounterparty: Boolean,
|
||||
val servesTrustedVenue: Boolean,
|
||||
val readTrustEnabled: Boolean,
|
||||
val hasAttributablePurpose: Boolean,
|
||||
)
|
||||
@@ -57,9 +60,9 @@ data class RelayAuthInputs(
|
||||
* - [RelayAuthPolicy.NEVER] → DENY
|
||||
* - [RelayAuthPolicy.ALWAYS] → ALLOW
|
||||
* - [RelayAuthPolicy.IF_IN_MY_LIST] → ALLOW if in my list, else fall through
|
||||
* - [RelayAuthPolicy.TRUSTED_FOLLOWS] → ALLOW if in my list, or a followed counterparty is
|
||||
* served for a write purpose (DM/notification), or (when [RelayAuthInputs.readTrustEnabled])
|
||||
* for a read purpose; else fall through
|
||||
* - [RelayAuthPolicy.TRUSTED_FOLLOWS] → ALLOW if in my list, a venue the user joined/follows is
|
||||
* served, a followed counterparty is served for a write purpose (DM/notification), or (when
|
||||
* [RelayAuthInputs.readTrustEnabled]) for a read purpose; else fall through
|
||||
* 4. Fall-through → [RelayAuthVerdict.ASK] when the purpose is known, otherwise DENY.
|
||||
*/
|
||||
object RelayAuthResolver {
|
||||
@@ -80,6 +83,7 @@ object RelayAuthResolver {
|
||||
if (inputs.isInMyRelayList) RelayAuthVerdict.ALLOW else fallThrough(inputs)
|
||||
RelayAuthPolicy.TRUSTED_FOLLOWS ->
|
||||
if (inputs.isInMyRelayList ||
|
||||
inputs.servesTrustedVenue ||
|
||||
inputs.servesFollowedWriteCounterparty ||
|
||||
(inputs.readTrustEnabled && inputs.servesFollowedReadCounterparty)
|
||||
) {
|
||||
|
||||
+9
@@ -31,6 +31,7 @@ class RelayAuthResolverTest {
|
||||
isInMyRelayList: Boolean = false,
|
||||
servesFollowedWriteCounterparty: Boolean = false,
|
||||
servesFollowedReadCounterparty: Boolean = false,
|
||||
servesTrustedVenue: Boolean = false,
|
||||
readTrustEnabled: Boolean = false,
|
||||
hasAttributablePurpose: Boolean = true,
|
||||
) = RelayAuthInputs(
|
||||
@@ -40,6 +41,7 @@ class RelayAuthResolverTest {
|
||||
isInMyRelayList = isInMyRelayList,
|
||||
servesFollowedWriteCounterparty = servesFollowedWriteCounterparty,
|
||||
servesFollowedReadCounterparty = servesFollowedReadCounterparty,
|
||||
servesTrustedVenue = servesTrustedVenue,
|
||||
readTrustEnabled = readTrustEnabled,
|
||||
hasAttributablePurpose = hasAttributablePurpose,
|
||||
)
|
||||
@@ -92,6 +94,13 @@ class RelayAuthResolverTest {
|
||||
assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(servesFollowedReadCounterparty = true, readTrustEnabled = true)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trustedFollowsAllowsVenueYouJoinedOrFollow() {
|
||||
// A public chat / community / live stream you've joined (or whose owner you follow) —
|
||||
// auto-auth for both reading and posting, regardless of the read sub-toggle.
|
||||
assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(servesTrustedVenue = true, readTrustEnabled = false)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trustedFollowsFallsThroughForStranger() {
|
||||
// Not my relay, no followed counterparty -> prompt when we know why, else silent deny.
|
||||
|
||||
Reference in New Issue
Block a user