Commit Graph
16610 Commits
Author SHA1 Message Date
Claude 3a96554c8d feat(relayauth): replace the policy list with Always / Never / Custom + toggles
Restructure the global auth control into a top-level mode — Always authenticate,
Never authenticate, or Custom — where Custom reveals independent per-situation
toggles instead of the confusing single sub-toggle:

- My relays and venues (own relays + joined/subscribed/favorited venues) — on
- Read posts from people I follow — on
- Message people I follow (DMs, replies, notifications) — on
- Message anyone / strangers — off by default (you're asked each time instead)

RelayAuthPolicy is now {ALWAYS, NEVER, CUSTOM}. The resolver takes a
RelayAuthCustomToggles plus split serves-facts (followed-read, followed-write,
stranger-write, own-relay, venue) and, under CUSTOM, allows if any enabled
category matches — else falls through to a prompt. There is deliberately no
"read strangers' posts" category, so that always prompts.

Account settings replace the single delivery flag with four persisted booleans
(default policy CUSTOM; no migration, unreleased). The contextual DM/notification
prompt button now switches to CUSTOM and enables both message toggles. Resolver
tests rewritten per-toggle, including that reading a stranger is never
auto-allowed. Old IF_IN_MY_LIST / TRUSTED_FOLLOWS policies and their strings are
removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 9ca1ba2f5c feat(relayauth): reframe follows trust as relationship vs. message delivery
Restructure the two TRUSTED_FOLLOWS controls so they read as independent ideas
instead of a confusing read/write sub-toggle:

- The "My relays and people I follow" policy now trusts a followed user as any
  counterparty — reading their posts AND reaching them (DM/notification) — plus
  your own relays and joined venues. Reading your follows is no longer gated.
- The sub-toggle is repurposed to "Also log in to deliver my messages": trust a
  relay to send DMs, replies or notifications to anyone you're talking to, even
  people you don't follow.

Resolver: replace servesFollowed{Write,Read}Counterparty with a single
servesFollowedCounterparty, add servesWriteCounterparty (an inbox of anyone
you're messaging), and gate the latter behind the new
messageDeliveryTrustEnabled input. Rename the account setting
relayAuthTrustFollowsForReads -> relayAuthTrustMessageDelivery (+ pref key;
no migration needed, unreleased). The contextual prompt button moves from
read-post prompts to DM/notification prompts and now enables message delivery.
Resolver tests updated for the new inputs, including that the delivery toggle
is write-only and never auto-allows reading a stranger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 3e6cda8407 feat(relayauth): modernize the per-relay list to match the app's relay rows
Restore the "Per-relay overrides" heading and rebuild each relay card using the
same conventions as the other relay screens: the relay's NIP-11 icon (robohash
fallback), the shortened displayUrl instead of the raw URL, last-used as a
subtitle under the name, and the whole card tappable to open the relay's
NIP-11 info screen (Route.RelayInfo). The Allow/Deny chip and Forget stay on
the trailing edge; the served-people facepile sits on its own row below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 7c58ea31fd feat(relayauth): merge override + rationale into one per-relay list
Fold the separate "Why you're logged into these relays" section into the
per-relay list so each relay is one card: URL, an Allow/Deny chip, a facepile
of the people it serves (3 avatars + "+N"), and when it was last used.

Collapse the two removal actions (the override "X" and the rationale "Forget")
into a single Forget that clears both the stored decision and the recorded
reason, so the relay drops off the list — this removes the earlier ambiguity
about what each button did. The Allow/Deny chip now also shows for relays
allowed by policy (no explicit override), so they can be blocked from here too.

Also clarify the confusing read-trust sub-toggle copy so it reads as the
read-side extension of the write-only follows policy ("On its own, the option
above only logs in to send messages or notifications… turn this on to also log
in when downloading their posts"). Drop the three now-unused strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 44462ecce1 fix(relayauth): reword follow-trust button to "Always allow my follows' relays"
Shorter and clearer than "Always allow relays from my follows".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude e9dad44131 fix(relayauth): shorten the follow-trust button label
"Always allow relays of people I follow" overflowed the button; shorten to
"Always allow relays from my follows".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 620eeadc2b feat(relayauth): one-tap "trust my follows' relays" on read-post prompts
On a "Load posts from <user> and others" (READ_OUTBOX) auth prompt, add a
button that turns on the broad rule instead of allowing one relay at a time:
it sets the policy to TRUSTED_FOLLOWS and enables the read-trust sub-toggle,
so every relay serving people the user follows auto-authenticates for reads
and these prompts stop appearing. Read-trust only applies under
TRUSTED_FOLLOWS, so the button sets both to keep its promise on any prior
policy, then authenticates the current connection once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 672fb931c8 test: integration-test NIP-17 DM delivery through an auth-required relay
Drive a real NostrClient against an in-process geode relay running
FullAuthPolicy, publishing a NIP-17 gift wrap through the PoolEventOutbox
retry queue. The first EVENT races ahead of AUTH and is rejected
`auth-required`; a RelayAuthenticator answers the challenge and the
still-pending wrap is resent on the post-AUTH resync and stored. This is
the integration counterpart to PoolEventOutboxAuthTest and exercises the
"auth-required must not burn the retry budget" fix end-to-end. A control
test (no authenticator) proves the relay genuinely gates, so the delivery
assertion isn't vacuous.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 893b3c7140 test: cover DataStoreRelayAuthPermissionStore round-trips and pruning
Add JVM unit tests (real PreferenceDataStore on a per-test temp dir, no
Robolectric) covering: decision round-trip and hashed-key reverse lookup;
recordUse counterparty merge by kind, the 64-counterparty cap, idempotence,
and that a new counterparty always persists despite the write throttle; and
the reverse-lookup pruning contract — clearRationale/clearDecision prune the
shared url key only when nothing else references the relay, keeping it when a
decision or rationale still remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude e959acb215 refactor: extract testable AuthDecisionResolver from AuthCoordinator
The verdict-folding policy (no-ledgers auto-allow, any-ALLOW short-circuit,
ASK→prompt→remember Allow/Block) was inlined in AuthCoordinator's signing
lambda, untestable without the relay client and signers. Move it to a pure
suspend AuthDecisionResolver.resolve(verdicts, prompt) returning an Outcome
(shouldAuth + optional remembered override), and cover every branch with
unit tests, including that ASK never prompts when an account already allows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 72f1f72ea7 refactor: share user-load and reason-string helpers across relay-auth UI
The auth prompt dialog (RelayAuthPromptHost) and the relay-auth settings
screen each carried an identical copy of a pubkey→User loader and the
AuthPurposeKind→reason-string mapping. Extract both into
RelayAuthComposeHelpers (LoadRelayAuthUser, relayAuthReasonRes) and point
both call sites at the shared versions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude a8fe2eee92 refactor: use quartz typed tag parsers in auth purpose derivation
Replace hand-rolled tag-index parsing in RelayAuthPurposeDeriver with
quartz's typed parsers: PTag.parseKey, ATag.parseAddress (filtered by
community/live-activity venue kinds), MarkedETag.parseRoot / ETag.parseId
for channel roots, and Address.parse for filter `a`-tags. Derive the
venue owner pubkey via Address.parse(...).pubKeyHex instead of splitting
the address string by hand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude 95e3a8a54d fix(relayauth): load and observe venue names in the auth prompt
rememberVenueLabel was a non-loading cache snapshot, so a venue not yet
in LocalCache showed its id and never fetched or updated. Bring it to
parity with the person path (checkGetOrCreateUser + observeUserInfo):
get-or-create the public chat / live activity channel and observeChannel
it, which subscribes to relays for the metadata and recomposes when the
title arrives. Communities keep the NIP-72 d-identifier (no fetch needed).

Audit of the rest confirmed correct: person names (rememberDisplayName /
LoadUser / UsernameDisplay) already load + observe; the trust predicates
read .value of Eagerly-shared StateFlows (publicChatList/communityList/
allFollows), which is the right decision-time snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude ffe9637c8d feat(relayauth): resolve real venue names in the auth prompt
The venue prompt said generic "this room"; now it names the actual place.
rememberVenueLabel resolves a public chat channel's title (by event id),
a live activity's title (by address), or a community's d-identifier (its
NIP-72 name), falling back to the address d-tag / short id when the venue
isn't cached. So the prompt reads "Post to nostr-dev?" / "Open <stream
title>?" with a matching consequence ("…your message to nostr-dev won't
be posted."), reusing the same who-slot as the person purposes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude 3526a14ebe 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
2026-07-10 22:40:11 +00:00
Claude eea072ec42 feat(relayauth): follow-trust default + action-aware prompt copy
User-perspective fixes so the auth prompt stops feeling like "why does
this app keep asking me to log into things":

1. Default policy is now TRUSTED_FOLLOWS (new installs only; a persisted
   choice is untouched). Messaging/notifying people you follow just works;
   only strangers prompt.
2/3. The prompt is reframed around what the user was doing and its
   consequence, not "log in":
   - Title is action-aware: "Send your message to Alice?" / "Notify …?" /
     "Load posts from …?", resolving the counterparty's display name.
   - The message states the real tradeoff (the relay confirms it's you;
     its operator sees which account you are) instead of "log in".
   - A purpose-specific, error-tinted consequence line — "If you don't,
     your message to Alice won't be delivered." — so Block/Dismiss no
     longer silently break the exact thing the user was trying to do.
   Per-purpose reason labels now show only when a challenge spans multiple
   purposes (the title carries the single-purpose case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude af0274bb24 style(relayauth): modernize the NIP-42 auth prompt dialog
Light design pass on the ASK dialog (the weakest surface):
- Add the M3 AlertDialog icon slot with a primary-tinted shield, so it
  reads as a security decision.
- Give the actions a real hierarchy: filled "Allow once", tonal "Always
  allow this relay", and an error-tinted text "Block this relay" (it was
  three same-emphasis buttons before).
- Show the relay in a rounded surfaceVariant chip instead of raw bold text.
- Keep named avatar rows for ≤2 counterparties (the common DM case) and
  collapse larger sets into an overlapping avatar facepile with a "+N"
  overflow badge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude cface58e15 perf(relayauth): lazy auth context + prune stale auth prompts
Audit follow-ups #3 and #5:
- AuthCoordinator builds the RelayAuthContext lazily, so the no-ledgers
  auto-allow path no longer pays for activeOutboxEvents/activeRequests +
  purpose derivation on every challenge.
- RelayAuthPromptBus now completes the deferred with DISMISS on timeout
  (it previously returned DISMISS without resolving it), and RelayAuthPrompt
  exposes isResolved/onResolved. RelayAuthPromptHost drops a prompt as soon
  as it resolves by any path (answered, answered elsewhere, or timed out)
  and skips any already-resolved prompt, so a stale dialog is never shown
  for a relay whose auth already gave up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude fb30982f92 perf(relayauth): bound grant rationale and throttle its writes
Audit fixes for the grant-rationale path:
- Bound counterparties per relay+purpose in the store (cap 64). An outbox
  relay can serve a large slice of the follow list; recording every author
  bloated the DataStore value and, since recordGrant stores every purpose
  that has counterparties, this happened even when the grant was for a DM.
- Throttle recordUse: auth is re-granted on every reconnect, so skip the
  DataStore write unless a new counterparty appeared or the last-used
  timestamp is stale (>5 min), instead of writing on every grant.
- Cap the settings rationale rows at 8 (+ "…and others"); they render in a
  plain non-lazy scroll column, so an unbounded set could compose hundreds
  of user rows.

The live follow-trust decision still sees the full counterparty set (the
cap is storage/display only), so trust verdicts are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude c29374f089 feat(relayauth): surface failed sends + per-relay forget/last-used
Consume the new give-up signal: RelayPublishFailureToastSubscription
(hosted in LoggedInPage) listens for onEventGaveUp and toasts "couldn't
deliver to <relay>", so a dropped send is visible instead of silent.

Rationale polish in the auth settings screen: each relay card now shows
"Last used N ago" and a Forget button that clears both the ALLOW/DENY
override and the accumulated rationale for that relay. The store gains
clearRationale + allLastUsed (default-implemented on the interface) and
records a last-used timestamp on each grant; clearDecision/clearRationale
now prune the shared url key only when a relay has neither an override
nor rationale left, so a partial clear never orphans the reverse-lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude 9179bd9d8d feat(quartz): signal when an outgoing event exhausts its retry budget
Instead of silently dropping an event once its per-relay retry budget is
spent, PoolEventOutbox now reports it: PoolEventOutboxState.newTry returns
whether the attempt gave up on the relay, PoolEventOutbox.onSent surfaces
the dropped event, and NostrClient notifies a new (default no-op, so
non-breaking) RelayConnectionListener.onEventGaveUp(relay, event). Lets a
host surface a failed delivery rather than lose it silently; the event may
still be pending on other relays. Unit-tested via the outbox try budget.

Note: timed retry backoff (the other half of this item) is intentionally
deferred — applied in the shared syncState path it would also delay the
post-auth resend and regress the auth-required fix, so it needs
trigger-aware handling designed separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude b1b7a4e7c3 fix(relayauth): exclude event author from NOTIFY_INBOX counterparties
A pending non-gift-wrap event's `p` tags are the people it notifies, but
some events self-p-tag the author. Drop the author's own key so the auth
reason and follow-trust check reflect who is actually being notified, not
the sender. (Own-relay over-attribution is already handled upstream by the
isInMyRelayList allow.) Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude 39dc053bf7 feat(relayauth): user toggle for trusting follows' outboxes on reads
Add relayAuthTrustFollowsForReads (local AccountSettings flag, persisted
via LocalPreferences, default off) and wire it into the ledger's
readTrustEnabled input. The relay-auth settings screen shows a switch —
"Also trust when reading their posts" — under the Trusted-follows policy,
so downloading a followed author's outbox auto-authenticates only when
the user opts in; writes (DMs/notifications) remain auto-trusted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude 70b23b6999 feat(relayauth): remember and show why each relay was granted
Extend RelayAuthPermissionStore with grant rationale (purpose ->
counterparty pubkeys), default no-op so other implementers are
unaffected; DataStoreRelayAuthPermissionStore persists it per relay,
merging counterparties across grants. AuthCoordinator records the
rationale via ledger.recordGrant whenever it authenticates (auto-allow,
override, or approved prompt). The relay-auth settings screen adds a
"Why you're logged in to these relays" section: per relay, purpose-
grouped rows ("Send your private message to:", "Download posts from:")
with each counterparty's avatar + name. Unit-tested: grouping, cross-
grant merge, and that counterparty-less purposes aren't recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude e128a7834b feat(relayauth): interactive "log in to this relay?" prompt dialog
RelayAuthPromptHost collects RelayAuthPromptBus.prompts app-wide (hosted
in LoggedInPage next to RelayAuthSubscription) and shows one dialog at a
time when a NIP-42 challenge resolves to ASK. The dialog explains why the
relay wants auth — grouped by purpose (send DM / notify / download posts)
with each counterparty's avatar + name — and offers Allow once / Always
allow this relay / Block this relay. Dismissing answers DISMISS, matching
the bus timeout fallback so a connection never blocks on the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude deb0abb982 docs: state the auth-required outbox fix narrowly
Clarify that resend-after-auth already worked for the common single-round
case; the fix only prevents auth-required NAKs from consuming the per-relay
retry budget, which could evict the saved event across repeated rounds
(slow signer / reconnect churn / re-challenge) at the moment syncFilters
tries to redeliver it post-auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude e857e1c8e8 feat(relayauth): suspendable ASK prompt mechanism for NIP-42 auth
Add RelayAuthPromptBus: when a challenge resolves to ASK, the auth
coroutine calls requestDecision() and suspends until the UI answers via
a SharedFlow<RelayAuthPrompt> + CompletableDeferred. Concurrent
challenges for the same relay share one prompt (no double-asking) and an
unanswered prompt times out to DISMISS so a connection never hangs.

AuthCoordinator now consults the prompt on ASK and acts on the choice:
ALLOW_ONCE signs once; ALWAYS_ALLOW / BLOCK persist a per-relay override
via the ledger and then sign / skip; DISMISS skips. Exposes promptBus so
a Composable can host the dialog (the visual layer is the next step).
Unit-tested: choice delivery, same-relay dedup, and timeout-to-DISMISS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude c036069439 feat(relayauth): wire purpose-aware resolver into the live auth path
AuthCoordinator now reconstructs the challenge purpose from what the
client is doing with the relay — pending outgoing events + active
subscription filters — via the new pure RelayAuthPurposeDeriver
(gift wrap => SEND_DM, p-tagged event => NOTIFY_INBOX, filter authors
=> READ_OUTBOX), and asks each account's ledger for a verdict through
RelayAuthResolver instead of the old URL-only ALLOW/DENY.

RelayAuthPermissionLedger gains context-aware decide(RelayAuthContext)
plus isBlocked / isFollowed / readTrustEnabled inputs; the live wiring in
AccountDataSourceSubscription splits the old combined check into separate
blocked-list and trusted-list predicates and feeds follow membership from
allFollows (any follow list). TRUSTED_FOLLOWS now auto-auths for relays
serving a followed DM/notification counterparty. ASK is treated as
"don't auth yet" pending the interactive prompt (next step).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 38b2e65362 feat(quartz): expose pending outbox events per relay for auth context
Add INostrClient.activeOutboxEvents(url) (backed by
PoolEventOutbox.activeOutboxEventsFor) returning the full events still
pending delivery to a relay, not just their ids like activeOutboxCache.
This lets a host explain *why* a relay is being authenticated with —
e.g. a pending kind-1059 gift wrap means we're sending a DM to its
recipient — by inspecting kind/tags. Combined with the existing
activeRequests(url) filters, it is the generic challenge context the
NIP-42 decision hook needs. Updates the INostrClient test fakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 4bd73596e3 feat(relayauth): add purpose-aware AUTH policy core + TRUSTED_FOLLOWS
Introduce the platform-neutral decision core for contextual NIP-42 auth
in commons/relayauth:
- AuthPurpose / AuthPurposeKind / RelayAuthContext describe *why* a relay
  wants auth (send DM, deliver notification, read outbox, own relay).
- RelayAuthVerdict adds an ASK outcome (runtime-only; never persisted,
  unlike the two-value RelayAuthDecision override).
- RelayAuthResolver is a pure, unit-tested precedence ladder: blocked
  list > per-relay override > policy > ASK-if-attributable-else-DENY.
- New TRUSTED_FOLLOWS policy: auto-auth for relays serving a followed
  counterparty on a write purpose (DMs/notifications), and — behind a
  read sub-toggle — read purposes; strangers fall through to ASK.

Wires the new enum value through the existing settings screen (new
option + strings, reusing the Group symbol) and the URL-only ledger path
(degrades to the my-list check until challenge context is plumbed).
Live prompt UI, grant-rationale persistence, and quartz challenge-context
plumbing are follow-up steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 9c618f9ba1 fix(quartz): don't drop outgoing events on NIP-42 auth-required NAKs
PoolEventOutboxState treated an `auth-required` OK-false the same as any
transient failure, recording it against the per-relay Tries budget
(responses > 2 drops the event on the next send attempt). Relays that
NAK every unauthenticated EVENT could therefore exhaust the budget and
drop the message before the AUTH handshake completed — the event was
gone by the time syncFilters re-sent it after the auth OK.

Treat `auth-required` as a deferred state instead: keep the relay in
relaysRemaining and record no failure, so the existing
syncFilters-after-auth path redelivers it. Mirrors the behavior already
present in StandaloneRelayClient. Terminal rejections (invalid/pow/
replaced/deleted) and ordinary transient errors are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 6c9259fea0 docs: split AUTH permission plan into quartz mechanism vs amethyst policy
Account for the existing (rudimentary) outgoing-event retry in
PoolEventOutbox: syncFilters already re-sends EVENTs after an auth OK,
but auth-required is wrongly treated as a burned retry (hard >2/>3 cap,
no backoff, silent drop) — a race that can drop a message before AUTH
completes. Recommend fixing the resend/retry mechanism generically in
quartz (port StandaloneRelayClient's auth-required handling, add backoff
+ terminal give-up signal, enrich the injected auth-decision hook with
pending-event/active-filter context) and keeping only policy, purpose
derivation, ASK prompt, and grant-rationale UI in amethyst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 3570ebb014 docs: plan for contextual NIP-42 AUTH permissions
Design doc for asking the user *why* an auth is requested (send DM,
deliver notification, read outbox), a follow-based auto-trust policy
mode, an ASK decision state, and per-relay grant rationale shown in the
auth settings screen. Reuses the existing RelayAuthenticator /
AuthCoordinator / RelayAuthPermissionLedger stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:08 +00:00
Vitor PamplonaandGitHub 4d16d6e38f Merge pull request #3522 from vitorpamplona/claude/richtext-trailing-whitespace-85eudl
Strip trailing whitespace from rich text parser output
2026-07-10 18:11:21 -04:00
Claude 24ea909221 fix: remove trailing whitespace and newlines when rendering RichText
Trailing spaces and newlines in a note's content produced empty trailing
paragraphs after the line split, each rendered as a blank FlowRow line
between the last visible word and the end of the component. Trim the
content's tail before segmenting, and return no paragraphs for
whitespace-only content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZET7ig8MvR5vSCSxvcURi
2026-07-10 22:09:11 +00:00
Vitor PamplonaandGitHub 581fded5f5 Merge pull request #3521 from vitorpamplona/claude/brainstorm-world-client-033mhn
Update Brainstorm web app icon URL
2026-07-10 18:06:24 -04:00
Claude 96965305cf fix: use Brainstorm's real apple-touch-icon in browser suggestions
https://brainstorm.world/brainstorm.svg 404s (the SPA serves its HTML
not-found page with a 200), so the Discover row fell back to the globe
glyph. Point at the site's declared 180x180 apple-touch-icon.png instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzQhmuFNULgiP31XqMRpJP
2026-07-10 21:53:49 +00:00
Vitor PamplonaandGitHub 193a5559bd Merge pull request #3516 from vitorpamplona/claude/amethyst-apk-cert-docs-dy2pzo
docs: Add APK signature verification instructions
2026-07-10 16:34:04 -04:00
Vitor PamplonaandGitHub c06c6523b5 Merge pull request #3518 from vitorpamplona/claude/amethyst-trim-post-whitespace-t9xi5m
Trim message text before tagging in post ViewModels
2026-07-10 16:25:54 -04:00
Vitor PamplonaandGitHub 0f89fdb7e1 Merge pull request #3517 from vitorpamplona/claude/amethyst-login-theme-toggle-5af214
Persist login UI state across configuration changes
2026-07-10 16:25:39 -04:00
Claude a509d41358 fix: keep login/signup page choice across uiMode recreation (#1097)
Toggling the system dark/light theme recreates MainActivity (uiMode is
not in configChanges), and the login/signup page choice was held in a
plain remember, so the UI reset from Sign-Up back to Login. Switch it to
rememberSaveable so the choice survives recreation. Same fix for the
password-visibility toggle in LoginScreen.

https://github.com/vitorpamplona/amethyst/issues/1097

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J74bRJ9H1KFx4vTo9wpez3
2026-07-10 20:11:47 +00:00
Claude 03cb5e4123 fix: trim leading/trailing whitespace from composed posts (#349)
Trim the composer text once, where it is handed to NewMessageTagger, so
every event kind built from tagger.message (short note, poll, zap poll,
NIP-29 thread, NIP-22 comment) publishes without stray leading/trailing
whitespace and mention rewriting operates on the exact string that gets
published. The now-redundant per-call trim on the NIP-29 thread build is
removed. Whitespace-only messages are still rejected by the existing
isNotBlank gates before a template is ever built.

https://github.com/vitorpamplona/amethyst/issues/349

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1V6v7aYBE8B5PhNZMNs3o
2026-07-10 20:11:08 +00:00
Claude 412ec16905 docs: publish APK signing certificate SHA-256 for verification (#972)
Adds a 'Verifying the APK signature' section to the README so users
sideloading via Obtainium/AppVerifier/GitHub Releases can check the
signing certificate of their download.

The fingerprint was COMPUTED (not transcribed) from official release
artifacts of the latest release, v1.12.6:

- amethyst-fdroid-arm64-v8a-v1.12.6.apk
  https://github.com/vitorpamplona/amethyst/releases/download/v1.12.6/amethyst-fdroid-arm64-v8a-v1.12.6.apk
  (file SHA-256 ed95fdb39d668ad2100ca6ff90f21617537790c8db47850f36ca36f75bc7aeea,
  matching the GitHub release asset digest)
- amethyst-googleplay-arm64-v8a-v1.12.6.apk
  https://github.com/vitorpamplona/amethyst/releases/download/v1.12.6/amethyst-googleplay-arm64-v8a-v1.12.6.apk
  (file SHA-256 dacc6db1ba14c914504c6629f8fc039de0918a2ea9d35483915b4721391c3b19,
  matching the GitHub release asset digest)

Extracted with 'apksigner verify --print-certs' (build-tools 37.0.0)
and cross-checked with 'keytool -printcert -jarfile'. Both flavors are
signed with the identical certificate (CN=Vitor Pamplona, O=Amethyst
Labs), consistent with create-release.yml signing both flavors with
the same SIGNING_KEY secret:

  SHA-256: c2d0aa86bcb6b62090561a41bbe336e98b78c2d0210a498dc885f28e1348cf17

Fixes #972

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148shLViyE1KjFc69J6xYux
2026-07-10 20:02:35 +00:00
Vitor PamplonaandGitHub 16711caec0 Merge pull request #3515 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-10 13:57:30 -04:00
vitorpamplonaandgithub-actions[bot] 085b4d7dc0 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-10 17:06:25 +00:00
Vitor PamplonaandGitHub 525d3ef189 Merge pull request #3514 from vitorpamplona/claude/armada-nip29-integration-lwqard
Relay Groups: NIP-29 relay-based group chat (Armada interop)
2026-07-10 13:03:48 -04:00
Claude f946e1aaf4 fix: use the Topic icon for the composer's subject toggle
The subject/title toggle used the Article (document-lines) glyph, which reads as
"body text". Switch to Topic — a clearer signifier for a subject/title line.
Codepoint already present in MaterialSymbols, so no font-subset regen needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:50:23 +00:00
Claude 003142122b fix: don't drop NIP-11 waiters on a stuck Loading marker
The relay-signed NIP-29 gate reads each relay's NIP-11 `self` key from the
cache. Nip11CachedRetriever.loadRelayInfo treated a valid `Loading` marker as
"a fetch is already in flight, just wait" — but it dropped the caller's
callback entirely and had no way to notify it when the fetch finished.

That is a lost-wakeup: if the coroutine that started the fetch is cancelled
mid-flight (e.g. the discovery screen that launched the warm-up leaves
composition when you tap a relay chip), the `Loading` marker is left behind,
valid for a full hour. The relay's on-group-list screen then mounts, sees the
stuck `Loading`, waits forever, and never receives the doc — so its NIP-11
looks empty (no `self`), the self-key gate rejects every 39000, and a group
you can plainly see under the "Mine" filter (which bypasses the gate) vanishes
on the relay page.

Re-fetch on `Loading` instead of silently waiting: the fetch is cheap, dedups
at the HTTP layer, and guarantees this caller is notified. Error caching is
unchanged (still avoids hammering a genuinely broken relay).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:27:02 +00:00
Claude af0e9c512e fix: warm host-relay NIP-11 in discovery so All-Follows shows their groups
Companion to the All-Follows roster fix: the discovery screen warmed NIP-11
only for the outbox `candidateRelays`, but a group's relay-signed 39000 lives
on its HOST relay. In All-Follows the subassembler now probes those host
relays (joined kind-10009 + favorited kind-10012) for follow rosters, so their
39000s land in the cache — but the self-key gate (isRelaySignedRelayGroup)
then read an unwarmed, empty NIP-11 (self=null) and dropped every one of them.

That is why the groups only appeared after bouncing through Global (whose
relay set happens to include the host relay, warming it as a side effect).
Warm the joined + favorited host relays alongside the outbox candidates so
the gate is a cache hit with the relay's `self` key present.

Verified against wss://basspistol.org: NIP-11 advertises nip-29 with
self=afd7da3f…, all six 39000s are signed by that self key, and
`amy relaygroup browse` returns all six with unverified_dropped=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:15:28 +00:00
Claude 4d81d298ec fix: load follow-participated relay groups in All-Follows without Global
The All-Follows / Authors discovery filter sets resolve their relays via
the outbox model (each follow's own publish relays), but a NIP-29 roster
(kind-39001 admins / 39002 members) lives ONLY on the group's host relay.
So a follow who is an admin or member of a group never surfaced in
All-Follows until the user bounced through Global — which pulls the whole
directory — and back.

Additionally query the follows as `#p` against the group-host relays we
already know about (joined via kind-10009 + favorited via kind-10012),
minus the relays the outbox filter already covers, and re-assemble when
either list changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:08:21 +00:00