From 3426b8373943bcf7bd762a12d7a2ce29208c6334 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 16 Aug 2026 22:36:00 -0400 Subject: [PATCH] fix: honour "always log in" for relays the account does not use itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RelayAuthResolver gated the ALWAYS policy behind isFirstParty: RelayAuthPolicy.ALWAYS -> if (inputs.isFirstParty) ALLOW else fallThrough(inputs) so "Always log in" only auto-authenticated relays the account had its own reason to be on. Any relay reached only through somebody else's traffic — a followed author's outbox, another logged-in account — fell through to a prompt. With the outbox model dialling 250+ relays and no stored per-relay decisions yet, that is one prompt per third-party relay on a fresh install. Narrowing to "only the relays I use" is what the "decide per relay" option (CUSTOM plus RelayAuthCustomToggles) exists to express; applying it to ALWAYS as well left no way to say "just authenticate everywhere", and contradicted both the enum's own KDoc and the setting's description ("Every relay that asks."). Make ALWAYS unconditional and keep the first-party gate on CUSTOM, where it belongs. Blocked relays (kind 10006) and explicit per-relay overrides still take precedence — they are resolved before the policy. The old behaviour was pinned by a test that documented it as intentional; replaced with one asserting ALLOW either way, plus a new test keeping the first-party gate covered under CUSTOM. Co-Authored-By: Claude Opus 5 (1M context) --- .../commons/relayauth/RelayAuthResolver.kt | 19 +++++++++++++------ .../relayauth/RelayAuthResolverTest.kt | 19 +++++++++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolver.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolver.kt index d9030c0889..3d095ca38b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolver.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolver.kt @@ -92,11 +92,14 @@ data class RelayAuthInputs( * else fall through * 4. Fall-through → [RelayAuthVerdict.ASK] when the purpose is known, otherwise DENY. * - * Both automatic grants in step 3 additionally require [RelayAuthInputs.isFirstParty]: an account - * never reveals its identity *without being asked* on a relay it has no reason of its own to be on. - * That is what keeps a bystander account off a relay only another account uses. It deliberately does - * not suppress the question — a non-first-party challenge we can explain falls through to ASK, so - * "decide per relay" means the user decides rather than a silent denial they never see. + * The [RelayAuthPolicy.CUSTOM] grant additionally requires [RelayAuthInputs.isFirstParty]: under + * "decide per relay" an account never reveals its identity *without being asked* on a relay it has no + * reason of its own to be on, which is what keeps a bystander account off a relay only another account + * uses. It deliberately does not suppress the question — a non-first-party challenge we can explain + * falls through to ASK, so the user decides rather than getting a silent denial they never see. + * + * [RelayAuthPolicy.ALWAYS] is NOT gated this way: it means what it says, every relay that asks. Users + * who want the narrower "only the relays I actually use" behaviour choose CUSTOM. */ object RelayAuthResolver { fun resolve(inputs: RelayAuthInputs): RelayAuthVerdict { @@ -111,7 +114,11 @@ object RelayAuthResolver { return when (inputs.policy) { RelayAuthPolicy.NEVER -> RelayAuthVerdict.DENY - RelayAuthPolicy.ALWAYS -> if (inputs.isFirstParty) RelayAuthVerdict.ALLOW else fallThrough(inputs) + // Unconditional, by design: "Always log in" means every relay that asks. Narrowing it to the + // relays this account uses is what CUSTOM ("decide per relay") is for — gating ALWAYS as well + // left the user no way to express "just authenticate everywhere" and, on a fresh install with + // a large follow list, produced a prompt for each of the 250+ third-party outbox relays. + RelayAuthPolicy.ALWAYS -> RelayAuthVerdict.ALLOW RelayAuthPolicy.CUSTOM -> if (inputs.isFirstParty && customAllows(inputs)) RelayAuthVerdict.ALLOW else fallThrough(inputs) } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolverTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolverTest.kt index 52865acffc..9b97ddeea4 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolverTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayauth/RelayAuthResolverTest.kt @@ -146,11 +146,22 @@ class RelayAuthResolverTest { } @Test - fun nonFirstPartyNeverAutoAuthsUnderAlwaysPolicy() { - // "Always log in" is a statement about the relays this account uses. A relay it is only - // touching because of somebody else's traffic still has to be asked about. + fun alwaysPolicyAuthsEvenWhenNotFirstParty() { + // "Always log in" means every relay that asks, first-party or not — narrowing it to the relays + // this account uses is what CUSTOM is for. (This previously returned ASK for non-first-party, + // which left no way to express "authenticate everywhere" and prompted once per third-party + // outbox relay on a fresh install.) assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = true))) - assertEquals(RelayAuthVerdict.ASK, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = false))) + assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(policy = RelayAuthPolicy.ALWAYS, isFirstParty = false))) + } + + @Test + fun customPolicyStillRequiresFirstParty() { + // The first-party gate belongs to CUSTOM: a toggle that matches is not enough if the only reason + // we are on this relay belongs to somebody else. + val allOn = RelayAuthCustomToggles(myRelaysAndVenues = true, readFollows = true, messageFollows = true, messageStrangers = true) + assertEquals(RelayAuthVerdict.ALLOW, resolve(inputs(toggles = allOn, isInMyRelayList = true, isFirstParty = true))) + assertEquals(RelayAuthVerdict.ASK, resolve(inputs(toggles = allOn, isInMyRelayList = true, isFirstParty = false))) } @Test