Commit Graph

11876 Commits

Author SHA1 Message Date
Claude 310cae0a64 feat(cli): add amy marmot message react / delete verbs
Adds two inner-event verbs on top of the existing kind:9 text send path:
  amy marmot message react  GID TARGET_EVENT_ID EMOJI
  amy marmot message delete GID TARGET_EVENT_ID...

Both build an unsigned rumor per MIP-03 (RumorAssembler) and wrap it in
the usual kind:445 outer via MarmotOutboundProcessor — the same shape as
text messages, just a different inner kind. The target event is looked
up in the account's decrypted inner-message log (persisted by the
inbound pipeline) so the NIP-25 / NIP-09 templates can populate the
target's pubkey and kind tags without making the caller re-derive them.

Test 09 previously noted that react round-trip verification was blocked
on a missing CLI verb. Expanded it to react via amy and confirm B sees
the kind:7 surface in its messages stream.

https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
2026-04-23 20:50:47 +00:00
Claude e37899a115 feat(cli): add amy marmot reset safety-valve verb
Mirrors the Android Danger-Zone action: wipes every MLS group, retained
epoch secret, persisted KeyPackage bundle, relay subscription, and the
run-state since-cursors. Does not broadcast any SelfRemove/leave
commits, because the reset path is for recovering from corrupted or
unrecoverable local state where graceful teardown may be impossible.

Requires an explicit --yes to execute. Without --yes it emits a dry-run
JSON listing the groups that would be wiped and exits 2, so scripts
that forget the flag fail loudly instead of nuking state.

https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
2026-04-23 20:29:01 +00:00
Claude 61c01981cc feat(marmot): add Reset Marmot State safety valve in settings
Gives users a last-resort "nuclear" option when Marmot local state is
corrupted or otherwise unrecoverable — wipes every MLS group, retained
epoch secret, persisted KeyPackage bundle, subscription and in-memory
chatroom for the current account. Does not broadcast leave/SelfRemove
commits, since a graceful teardown may be impossible in exactly the
scenarios where users reach for this option. The KeyPackage is
republished lazily on the next sync so the account stays reachable.

Adds clearAllState() helpers on MlsGroupManager and
KeyPackageRotationManager, a resetAllState() orchestrator on
MarmotManager, an Account.resetMarmotState() entry point, and a
destructive row + confirm dialog in the AllSettingsScreen Danger Zone
that mirrors the existing Request-to-Vanish pattern.

https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
2026-04-23 20:08:26 +00:00
davotoula 647d6f9841 fix: remove duplicate @Suppress annotation on SearchFilterRow
Kotlin rejects the annotation being applied twice on the same function —
broke compileFdroidDebugKotlin and compilePlayDebugKotlin. Keeping the
one above @Composable, removing the duplicate below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:09:25 +02:00
davotoula 502e78bbd8 i18n: add translations for cs-rCZ, pt-rBR, sv-rSE, de-rDE
Fills in 60 newly-added English strings (always-on notifications, audio
rooms, battery optimization banner, bottom bar settings, drawer sections,
feed titles, search filters/scopes/sort modes, video player button
settings) across all four target locales, plus a handful of pre-existing
gaps (emoji_pack_count, interest_set_hashtag_count, badge_name_label,
new_community_relays_section) that were missing from individual locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:07:26 +02:00
Vitor Pamplona dffcd258b7 Merge pull request #2529 from vitorpamplona/claude/fix-key-rotation-restart-YVmfU
Quiet happy path logging, surface failures prominently
2026-04-23 14:08:18 -04:00
Claude 2ce652b97f chore(interop): quiet happy-path logs, keep detail in LOG_FILE
Trims ~300 lines of screen output from an all-pass run without losing
any post-mortem detail — all removed lines still land in $LOG_FILE:

- preflight: move tool-path listings to $LOG_FILE only
- configure_relays: collapse 30+ per-relay "add ok" info lines into a
  single "X ok, Y already-present, Z failed" summary
- configure_relays: drop verbose "B relay list after configure" dump
  (file only); compress kind:30443 / kind:10050+1059+445 sanity logs
  to single summary lines
- discover_a_relays: replace 10-line per-type relay list with a
  nip65/inbox/kp count summary; full lists in $LOG_FILE
- main: drop unconditional "Post-configure baseline diagnostics"
  (dump_daemon_diagnostics already fires on invite-timeout)
- test_02: move pre-invite dump_daemon_diagnostics and post-send
  dump_outbound_diagnostics into the failure branches only
- test_04: drop unconditional pre-invite daemon dump
- test_01: send raw relay-list blob to $LOG_FILE instead of stderr

Failure paths still dump full daemon diagnostics, so debugging a broken
relay set loses nothing.

https://claude.ai/code/session_011HLbt9QMwBMSxovNRkv4GB
2026-04-23 17:20:49 +00:00
Vitor Pamplona a84f2f1466 Merge pull request #2528 from davotoula/configurable-video-buttons
Configurable video buttons
2026-04-23 13:09:36 -04:00
Claude 346c34f2c9 fix(interop): drive test 13 via real MIP-00 consumption flow
Old test_13 asked the operator to restart Amethyst or toggle KP relays
to trigger rotation, but neither does in current Amethyst:
- Restart: ensureMarmotKeyPackagePublished early-returns when a bundle
  already exists on disk.
- Toggle KP relays: saveKeyPackageRelayList republishes the existing
  kind:30443 to the new relay set (same event_id).

Rotation only fires when A's own KeyPackage is *consumed* by an
incoming Welcome. Switch the test to have B create a one-off group
inviting A, which consumes A's KP and triggers the rotation path in
DecryptAndIndexProcessor -> publishMarmotKeyPackages. wn then sees a
new event_id for the same d-tag slot.

https://claude.ai/code/session_011HLbt9QMwBMSxovNRkv4GB
2026-04-23 17:06:29 +00:00
davotoula f6a68eecab Video Player Buttons now sits between Reactions and Zaps in the Account Settings section 2026-04-23 19:02:59 +02:00
Vitor Pamplona dda935c1e9 Merge pull request #2527 from vitorpamplona/claude/remove-group-on-leave-JTVQb
Fix memory leak when leaving Marmot groups
2026-04-23 12:12:37 -04:00
Claude 62f4599407 fix: drop Marmot chatroom on leave so messages clear from caches
When the local user leaves a Marmot MLS group, MarmotManager already
wipes MLS state, the relay subscription and the persisted message log,
but the chatroom (and its decrypted inner messages) lingered in
marmotGroupList until the UI happened to call removeGroup. The CLI
leave path skipped that step entirely, and removeGroup itself only
dropped the chatroom from rooms — it left noteToGroupIndex entries and
the chatroom's own messages set in place, so notes stayed strongly
referenced and kept appearing in the Notification feed.

Move the in-memory cleanup into Account.leaveMarmotGroup, and have
MarmotGroupList.removeGroup also clear the chatroom's messages and the
note→group index. LocalCache holds notes weakly, so cutting the strong
refs is enough — GC reclaims them and the existing acceptableEvent
filter (which keys off marmotGroupList.rooms) hides anything still
in-flight.
2026-04-23 16:07:14 +00:00
Vitor Pamplona da559dc4e8 Merge pull request #2526 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-23 11:44:07 -04:00
Crowdin Bot f4dc793bda New Crowdin translations by GitHub Action 2026-04-23 15:42:39 +00:00
Vitor Pamplona 9b820b892c Merge pull request #2525 from vitorpamplona/claude/fix-interop-tests-12-13-8IXpX
Fix undecryptable outer layer events being marked as processed
2026-04-23 11:41:13 -04:00
Claude 0ec1544d66 fix: keep undecryptable Marmot kind:445 events retryable after epoch advance
`MarmotInboundProcessor.processGroupEvent` added every event id to its
dedup set regardless of result, including `UndecryptableOuterLayer`. The
handler buffers those events for a post-commit retry, but the replay hit
the Duplicate early-return and skipped MLS decryption — so future-epoch
messages and group-metadata commits that arrived before their epoch's
commit were silently dropped until an app restart re-fetched them.

This is interop test 12 (offline catch-up): messages 6-8 and the rename
commit only appeared after restarting Amethyst.

Skip the dedup write on `UndecryptableOuterLayer` so retries actually
re-attempt decrypt once the epoch advances. Memory is still bounded by
the handler's per-group pending buffer (64 events, FIFO-evicted).
2026-04-23 15:39:56 +00:00
davotoula 78e21a0274 Code review:
Dead videoGroup parameter in inner RenderTopButtons overload. Confirmed via git show origin/main — the branch introduced it, and it's only passed through but never read inside the body (the outer overload uses
  videoGroup separately for VideoQualityPopup). Removed from signature and both call sites (preview + outer wrapper).

RenderTopButtons runs for every visible video tile, so recomputing the
top-bar / overflow action lists on every recomposition added avoidable
per-frame filter+map allocations. Memoize on the inputs that actually
change availability.

Also drop the outer Box around AnimatedOverflowMenuButton — the inner
OverflowMenuButton already provides its own sized anchor Box, so the
wrapper added no layout value.
2026-04-23 17:24:54 +02:00
Vitor Pamplona eae9a4d1b3 Merge pull request #2524 from vitorpamplona/claude/fix-marmot-chat-filter-cDP3C
Fix Marmot group message filtering for multi-account scenarios
2026-04-23 10:44:28 -04:00
Claude d0b2af752e feat: add configurable video player buttons
Mirror the existing reaction row settings pattern. Each of the six
configurable video player buttons (Fullscreen, Mute, Quality, Share,
Download, PictureInPicture) can be placed either in the top bar or in
the overflow menu, with drag-to-reorder within its chosen location.

- Data model: VideoPlayerAction, VideoButtonLocation, VideoPlayerButtonItem
  added to AccountSyncedSettingsInternal so the config syncs via NIP-78.
- StateFlow wrapper AccountVideoPlayerPreferences plumbed through
  AccountSettings, Account, and AccountViewModel.
- New settings screen at Route.VideoPlayerSettings mirroring the reaction
  row screen (drag-to-reorder list with per-row FilterChip location).
- RenderTopButtons partitions configured buttons into top-bar vs
  overflow lists; overflow icon is hidden entirely when empty. Existing
  availability rules are preserved (Fullscreen needs zoom handler,
  Quality needs HLS with multiple renditions, Download hidden on live
  streams, PictureInPicture hidden without device support).
- Volume is now observed via Player.Listener.onVolumeChanged so the
  overflow Mute row stays in sync with the current state.
- VideoQualityPopup extracted so the quality chooser can open from the
  overflow menu too.
2026-04-23 16:43:19 +02:00
Claude 79188e3f7f fix: scope Marmot messages in notification filter to the current account
The notification filter accepted any note whose `inGatherers` contained
a MarmotGroupChatroom, regardless of which account's list that chatroom
belonged to. Since notes live in the global LocalCache and accumulate
gatherer references across account switches (and are not cleaned up on
leave), old messages stayed in Notifications after switching accounts
or leaving a group.

Mirror the NIP-17 DM guarantee: only accept a Marmot message when the
gatherer is the same instance currently held by the active account's
`marmotGroupList.rooms[nostrGroupId]`.
2026-04-23 14:06:41 +00:00
Vitor Pamplona f93382daf9 Merge pull request #2523 from davotoula/improve-search-toggles-pt2
Improve search part 2
2026-04-23 09:09:21 -04:00
davotoula 8c41d7ea90 pasting an npub1... / nprofile1... / nevent1... / naddr1... / note1... in the search bar will navigate to the target instead of just showing search results. 2026-04-23 15:02:36 +02:00
Vitor Pamplona b84d286ba9 Merge pull request #2522 from davotoula/improve-search-toggles
Improve search toggles
2026-04-23 08:48:12 -04:00
Vitor Pamplona 4be16ae226 Merge pull request #2516 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-23 08:43:47 -04:00
Vitor Pamplona 0e2acd8886 Merge pull request #2521 from davotoula/claude/fix-r8-room-workdatabase
fix for: release/benchmark crash on start-up
2026-04-23 08:43:39 -04:00
Vitor Pamplona 7cff2c8344 Merge pull request #2520 from davotoula/claude/fix-hls-uri-extension-dJ5rR
fix(playback): route HLS URIs to HlsMediaSource via MediaItem mimeType
2026-04-23 08:43:24 -04:00
Vitor Pamplona 6d8b75c7a2 Merge pull request #2519 from nrobi144/feat/relay-power-tools
feat(desktop): Relay Power Tools — Dashboard, Config Editors, Subscription Wiring
2026-04-23 08:41:22 -04:00
davotoula 458d9bcbab compact filter row with bottom sheet for source/follows/sort
Replaces the two-row, mixed-control filter UI under the search bar with a
single segmented row for scope (All/People/Notes) and a Tune icon button
that opens a ModalBottomSheet containing Source, Follows-only, and Sort
controls. A primary-color dot on the icon indicates non-default filter
state.

Also renames SearchSortOrder.DEFAULT_EVENT/DEFAULT_PEOPLE to
EVENT_DEFAULT/PEOPLE_DEFAULT for consistency with EVENT_OPTIONS/
PEOPLE_OPTIONS, and routes the sheet's reset button through these
canonical constants so the UI default cannot drift from the ViewModel.
2026-04-23 14:20:14 +02:00
davotoula 3b0bf07e58 add missing Column import and spotless formatting 2026-04-23 14:19:57 +02:00
Claude 376cbf8d10 feat(search): add scope, source, follows, sort toggles + bech32 auto-resolve (Android)
Android search previously dumped users, notes, hashtags, relays, and 3 channel
types into a single untoggled list and silently relied on NIP-50 relay search.
This adds a second-row control strip so users can pick what they're looking for
and how results are sourced/ranked.

Commons:
- New SearchScope { ALL, PEOPLE, NOTES } and SearchSource { LOCAL, RELAYS }
- SearchSortOrder: add POPULAR; EVENT_OPTIONS now [RELEVANCE, NEWEST, POPULAR]

Android SearchBarViewModel:
- scope / source / followsOnly / sortOrder StateFlows
- searchResultsUsers + searchResultsNotes respect scope, follows, and sort
- POPULAR sorts notes by Note.zapsAmount (descending)
- Follows-only filters authors (includes replies since filter is by author)
- directEventResolver: bech32 nevent/note/naddr -> Route.Note (auto-navigate)
- updateDataSource skips relay emit when source == LOCAL

Android SearchScreen:
- Second row: [All | People | Notes] SegmentedButton + Sort dropdown
- Third row: [Local | Relays] SegmentedButton + Follows-only chip
- bech32 hit triggers nav.nav(route) and clears the search

Desktop parity deferred - Desktop already ships an advanced panel that covers
most of this (kinds/authors/dates/hashtags/language/exclude terms + sort +
bech32 direct lookup). The new enums live in commons so Desktop can adopt.

Build note: gradle 9.3.1 can't be downloaded in this environment, so Android
and Desktop compilation were not verified here.
2026-04-23 13:17:06 +02:00
davotoula ac1b197336 fix(proguard): keep no-arg constructor of Room *_Impl classes
R8 in playBenchmark (minifyEnabled) stripped the no-arg <init> from
WorkDatabase_Impl because -keepnames preserves names but not members.
Room instantiates generated *_Impl subclasses reflectively via
Class.getDeclaredConstructor(), so startup crashed with
NoSuchMethodException in androidx.startup.InitializationProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:11:49 +02:00
Claude 81d65e96a8 fix(playback): route HLS URIs to HlsMediaSource via MediaItem mimeType
ExoPlayer's URI-based content-type inference only fires for http(s)
schemes + known extensions, so HLS playback fails in two cases Amethyst
ships regularly:

  1. NIP-71 video events with BUD-10 blossom URIs. Hash-based URIs carry
     no extension and the imeta's application/vnd.apple.mpegurl never
     reaches the MediaItem — MediaItemCache dropped it.
  2. Bare BUD-10 URIs pasted into kind 1 posts, which have no imeta at
     all. Even though the URI contains `.m3u8` in its path, ExoPlayer's
     inference skips custom schemes (`blossom:`).

Forward a normalized mimeType onto MediaItem.Builder.setMimeType so
DefaultMediaSourceFactory routes to HlsMediaSource regardless of scheme
or extension:

  - Normalize IANA Apple HLS variants (application/vnd.apple.mpegurl,
    application/x-mpegurl, audio/x-mpegurl, audio/mpegurl) to
    MimeTypes.APPLICATION_M3U8.
  - Fall back to `.m3u8` URI detection when no imeta mimeType is
    present. Matches the existing isLiveStreaming(url) heuristic.

Adds diagnostic logs at MediaItemCache.compute and
CustomMediaSourceFactory.createMediaSource so the routing decision is
observable in logcat, matching the diagnostic-logs pattern already used
elsewhere in playback.
2026-04-23 12:28:21 +02:00
nrobi144 835947d611 feat(desktop): relay config persistence, correct counts, per-screen picker
- Persist relay list events (kinds 10050/10007/10006) as JSON to
  java.util.prefs.Preferences with per-account key isolation
- Load persisted relay configs on startup before bootstrap subscription
- Validate loaded events (kind + pubkey check), 8KB guard on writes
- Fix SearchScreen relay count: "0 of 1" not "0 of 7" — uses searchRelays
- Fix FeedScreen relay count: shows feed relay count, not all connected
- Per-screen relay picker dialogs: Dns icon on Feed and Search screens
  opens AlertDialog wrapping existing editors (Nip65RelayEditor,
  SearchRelayEditor) — no new composable files
- Fix created_at dedup: use >= for replaceable event semantics
- Fix setters: use TimeUtils.now() not Long.MAX_VALUE
- Add consumePublishedEvent() for local immediate update after publish
- Remove stale "not loaded" warnings from Search/Blocked editors
- Fix FeedHeader type: Set<NormalizedRelayUrl> not Set<Any>
- Fix picker LaunchedEffect(Unit) to not overwrite user edits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:00:03 +03:00
Vitor Pamplona 952d91a1fd Merge pull request #2517 from vitorpamplona/claude/fix-marmot-polling-R83Bs
Fix MLS UpdatePath filtering and improve interop harness diagnostics
2026-04-22 21:59:19 -04:00
Claude 3a2068a792 fix(marmot): buffer + retry UndecryptableOuterLayer events after epoch advance
Test 12 (offline catch-up) was almost passing: the retained-epoch
fallback (c88923a3) brought through the 5 epoch-1 application messages
the user sent before adding C, but 3 epoch-2 messages + the rename
commit still went missing. Only on app restart did they appear — the
relay re-delivered and the replayed arrival order happened to put the
epoch-2-advancing commit before the epoch-2 events.

Root cause: kind:445 events arrive from the relay in subscription
order, not epoch order. When B drives an offline-catch-up sequence
like

    msg-1..5 (epoch 1) → add C (epoch 1→2) → msg-6..8 (epoch 2) →
    rename (epoch 2→3)

the receiver can easily see {msg-6, msg-7, msg-8, rename, add-C,
msg-1..5}. Everything from the rename onward arrives before the
add-C commit, so the outer ChaCha20-Poly1305 layer fails with
UndecryptableOuterLayer — we don't have the epoch-2 exporter yet.
Quartz correctly reports the failure, but the receiver just logged
"likely from before our join" and dropped the event on the floor.
Nothing retried when the add-C commit eventually landed and the
epoch caught up.

Fix it at the GroupEventHandler level, where we can both see the
original GroupEvent and hook into every CommitProcessed:

  - Keep a bounded per-group ArrayDeque<GroupEvent> of events that
    failed with UndecryptableOuterLayer. FIFO-evict at 64 entries so
    a genuinely pre-join stream can't pin unbounded memory.

  - On every CommitProcessed for a group, drain that group's queue
    and re-run each event through the normal add() path. A successful
    retry may itself emit another CommitProcessed and trigger recursive
    draining — that's fine, epochs are strictly monotonic so recursion
    depth is bounded by the queue size.

  - Concurrent-safe: all queue mutation is guarded by a Mutex.

Ambient discriminator: we can't tell "epoch too old, from before our
join" from "epoch too new, commit hasn't arrived yet" just from
UndecryptableOuterLayer — both report it. Buffering both is fine:
truly-pre-join events stay buffered until FIFO eviction pushes them
out (no correctness problem, just a few dozen bytes of memory for a
bounded time).
2026-04-23 01:44:32 +00:00
Claude ab064d3cd3 Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt
2026-04-23 01:11:10 +00:00
Crowdin Bot 745f3781c3 New Crowdin translations by GitHub Action 2026-04-23 01:07:37 +00:00
Vitor Pamplona 7666ed5cc8 Merge pull request #2507 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 21:06:39 -04:00
Vitor Pamplona e12c7ca595 Merge pull request #2515 from vitorpamplona/claude/fix-marmot-reactions-gtBUg
Fix MIP-03 inner event verification in group messages
2026-04-22 21:06:04 -04:00
Claude 23573c0a5b Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-23 00:52:09 +00:00
Crowdin Bot 19a92e9bf5 New Crowdin translations by GitHub Action 2026-04-23 00:50:42 +00:00
Vitor Pamplona f721142c73 Merge pull request #2514 from vitorpamplona/claude/grant-user-privileges-JxW5T
Add admin privilege management for Marmot group members
2026-04-22 20:49:15 -04:00
Claude 5106e572a5 fix: consume Marmot inner reactions/deletions as verified
MIP-03 inner events inside a kind:445 GroupEvent are unsigned (sig is
empty or placeholder), so LocalCache's `justVerify()` fails and
`consume(ReactionEvent)` skips the branch that attaches the reaction to
its target note via `addReaction()`. The same applies to kind:5
deletions. Side-channel inner events from other Marmot clients
(WhiteNoise in particular) were therefore silently dropped by
LocalCache, so an inbound emoji reaction never appeared on the target
chat bubble.

NIP-17 has the same "unsigned inner rumor" shape and already hands the
inner event to `justConsume(..., wasVerified = true)`. Align Marmot with
that contract at the two inbound call sites:
- `GroupEventHandler.add` (fresh kind:445 from relay)
- `Account` restore-from-disk loop (persisted plaintext at app start)

Authenticity is already guaranteed by the MLS layer:
`MarmotInboundProcessor.processPrivateMessage` rejects any inner event
whose `pubkey` doesn't match the MLS sender credential identity before
ever emitting an `ApplicationMessage`.

https://claude.ai/code/session_01KhVtLJAEkNCwpRWUUTqDFM
2026-04-23 00:34:51 +00:00
Claude c88923a3d1 fix(mls): decrypt retained-epoch messages correctly (Test 12 offline catchup)
The `tryDecryptWithRetainedEpoch` fallback in `MlsGroupManager.decrypt`
exists precisely to cover the interop harness's Test 12 scenario —
application messages encrypted under epoch N arriving at a receiver
that has already processed a commit advancing to N+1. The primary
path throws "Message epoch X doesn't match current epoch Y", then the
manager iterates the retained epoch window and tries each.

Two bugs kept the fallback from ever succeeding:

1. sender-data ciphertext sample length. The primary decrypt path uses
   `MlsCryptoProvider.HASH_OUTPUT_LENGTH` (32 bytes, = KDF.Nh for
   HKDF-SHA256, per RFC 9420 §6.3.2); the retained path was using
   `AEAD_KEY_LENGTH` (16). The comment on the primary path — "Using
   AEAD.Nk here made sender-data decryption fail against every
   spec-compliant sender" — was the same fix just never propagated
   to this branch. sender-data AEAD always failed, the try/catch
   swallowed it, and the fallback returned null.

2. content plaintext was returned raw. AEAD output is a
   PrivateMessageContent struct (§6.3.1):
     `applicationData<V> || signature<V> || padding`.
   The primary decrypt path parses this with `pmcReader.readOpaqueVarInt()`
   to extract applicationData; the retained path returned the whole
   struct. Callers saw a length-prefixed blob with signature + zero
   padding glued on — obvious in a raw diff, invisible in a pass/fail
   test that only checked "decrypted is not null".

Fix both in `tryDecryptWithRetainedEpoch`. Existing quartz↔quartz
tests still pass because they don't exercise out-of-order decrypt —
this branch was effectively dead code before.

Add testDecryptRetainedEpoch_ApplicationMessageAfterCommit as a
regression: Bob encrypts at epoch N, Alice advances to N+1 via
add-member, Bob's epoch-N message then arrives at Alice — must round-
trip through the retained-epoch fallback and match the original
plaintext exactly.
2026-04-23 00:30:57 +00:00
Claude e6e3120cd2 feat(marmot): let admins grant/revoke admin privileges from group info UI
Adds a star toggle next to each member in MarmotGroupInfoScreen that
admins can use to promote a non-admin to admin or demote a fellow
admin. Both paths show a confirmation dialog and publish a
GroupContextExtensions commit that rewrites admin_pubkeys. The toggle
is hidden for non-admin viewers and for the signer's own row (self
demotion must go through the leave flow). The demote button is also
hidden when the member is the only remaining admin so we never hit the
MIP-03 admin-depletion guard in MlsGroup at commit time.

Account.grantMarmotGroupAdmin / revokeMarmotGroupAdmin reuse
updateMarmotGroupMetadata so the new admin list ships alongside the
signer's outbox relays, keeping the canonical relay set consistent
with other metadata commits.
2026-04-23 00:28:54 +00:00
Claude d05742a8b6 debug(marmot): log publishMarmotKeyPackages path for Test 13 diagnosis
Test 13 (KeyPackage rotation) fails with "no new KP event_id observed"
— the interop log shows three `needsKeyPackageRotation=true` triggers
(once per group join), each of which invokes
`account.publishMarmotKeyPackages()` from `processMarmotWelcomeFlow`,
but no subsequent `kind:30443` publish ever appears in the logcat.

`publishMarmotKeyPackages` was completely silent: no-op guards, the
needsRotation check, the rotated-events count, and the per-event
publish all ran without a log line. We can't tell whether the rotation
produces events that never reach a relay, produces zero events,
short-circuits on needsRotation=false, or is never called at all.

Mirror the addMarmotGroupMember log pattern: entry guards (manager
null / not writeable), the needsRotation + relay count at decision
time, the rotateConsumedKeyPackages produce-count, and a per-event
publish line with id + target relay list. Next rerun of Test 13 will
tell us which branch is silently dropping the rotation.
2026-04-23 00:18:56 +00:00
Claude a4e031353a Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-22 23:41:40 +00:00
Vitor Pamplona 95f189338a Merge pull request #2513 from vitorpamplona/claude/fix-whitenoise-reactions-EWq2j
Filter non-displayable messages from Marmot group chat feed
2026-04-22 19:39:42 -04:00
Vitor Pamplona ca4459ca1d Merge pull request #2512 from vitorpamplona/claude/fix-marmot-relay-routing-VuVUp
Implement per-recipient relay routing for Marmot protocol
2026-04-22 19:38:58 -04:00
Claude 3279c2463a fix(mls): use filtered direct path in UpdatePath per RFC 9420 §7.9
Interop Test 06 (wn removes a member from a group Amethyst is in) was
failing with:

  GroupEventHandler.add: ERROR Failed to apply commit:
    UpdatePath node count (1) doesn't match direct path length (2)

Repro scenario: 3-member group [B=leaf 0, C=leaf 1, A=leaf 2]; B (admin,
committer) removes C. After the Remove proposal applies, leaf 1 is
blank but leaf 2 (A) is still occupied — so leaf_count stays at 3 per
RFC 9420 §7.8 and the sender's direct path has length 2 ([node 1,
root]).

Per RFC 9420 §4.1.2 and §7.9 the **filtered direct path** drops any
parent node whose child on the copath has an empty resolution:
encrypting to such a parent is equivalent to encrypting to its only
non-blank child, which is already on the direct path. In our scenario
the parent at level 1 (parent of leaves 0, 1) has a blank leaf 1 on
the copath — empty resolution — so it's filtered out. Filtered
direct path length is 1, and that's what openmls / whitenoise-rs emit
in `UpdatePath.nodes<V>`.

Quartz was always using the unfiltered direct path on both sides
(send + receive + parent-hash chain + path-secret decrypt). That
worked for quartz↔quartz (both sides agreed), but broke the moment a
spec-correct sender (wn) sent a filtered UpdatePath into a quartz
receiver. Parent-hash chain is further affected because §7.9.2 walks
the filtered path — even if the size check passed, the hashes would
not match.

Fix it throughout:

- RatchetTree.filteredDirectPath: new helper returning parallel
  (filteredDirectPath, filteredCopath) with the empty-resolution
  entries dropped. Uses the existing `resolution()` helper, which
  already handles unmerged_leaves per §4.1.2.

- RatchetTree.applyUpdatePath: size-check + target the FILTERED path.

- MlsGroup.commit (sender): emit one staged path node per filtered
  level; encrypt path secrets using the filtered copath; patch parent
  hashes into the filtered positions only.

- MlsGroup.processCommitInner (receiver): the UpdatePath node index
  aligns to the filtered direct path, so the common-ancestor lookup
  must find the filtered index to pick the right ciphertext + copath
  resolution. Use the unfiltered index only for counting KDF steps
  to the root (path_secret chain advances one step per unfiltered
  level regardless of filtering — the chain is continuous; filtering
  only decides which levels emit a UpdatePathNode, not how many KDF
  steps separate them).

- MlsGroup.computeSenderParentHashes: walk the filtered direct path.
  Map each filtered node back to its unfiltered level so the
  preUpdateSiblingHashes lookup (indexed by unfiltered level) still
  resolves. This makes the parent_hash chain agree with §7.9.2 and
  with what openmls computes on the other side.

- MlsGroup.verifyParentHash: short-circuit on empty filtered path
  rather than empty unfiltered path.

New regression test: testRemoveMiddleLeaf_ReceiverAcceptsCommit
reproduces the 3-member remove-middle scenario end-to-end within
quartz. It passed before this patch (because both sides were
unfiltered and agreed with each other), and it passes after this
patch (because both sides are now filtered and still agree) — the
compatibility win is that quartz now also agrees with
openmls/wn-produced commits of the same shape.

Also fix an unrelated script bug in marmot-interop.sh: the
discover_a_relays SQL probe was falsely reporting "wn has NO cached
relay entries for A" even when welcome delivery was working, because
the query assumed users.pubkey stored as BLOB but wn stores it as
TEXT (hex) in the current schema. Accept either encoding.
2026-04-22 22:58:21 +00:00