Complete the "Let's be reasonable" content set:
- reports (1984) and torrents (2003/2004) — additive public events whose
reputational weight is no greater than the arbitrary kind-1 notes an app
can already publish.
- long-form articles (30023), wiki (30818), and the legacy addressable
video kinds (34235/34236) — addressable content. Re-signing with the same
d tag replaces the app's own prior version; accepted as no worse than the
arbitrary posting a kind-1 grant already permits.
Only replaceable *configuration* (profile 0, contacts 3, 10000-range lists)
stays ASK, since a bad write there can silently wipe account settings —
distinct from addressable content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo
Add the remaining regular, additive, public, plaintext content and
engagement kinds that sit in the same risk class as notes/pictures:
relay chat (9), threads (11), public messages (24), poll votes (1018) and
polls (1068), file metadata (1063), voice messages (1222) and replies
(1244), live-stream chat (1311), and code snippets (1337).
Documents the borderline kinds left at ASK on purpose: reports (1984) and
torrents (2003/2004) carry reputational/legal weight, and
addressable/replaceable content (long-form 30023, wiki 30818) can overwrite
prior versions. Test pins several of these exclusions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo
Add video posts (kinds 21 normal, 22 short) as direct siblings of picture
posts (20) — additive, public, non-destructive content in the same risk
class as the original note set.
Also auto-approve NIP-42 relay auth (22242): an ephemeral proof-of-key
bound to a single relay + challenge (unreplayable elsewhere) that
Amethyst's own client already auto-signs for every logged-in account, so
treating it as background noise for napplets matches existing behavior.
Deliberately still ASK: NIP-98 HTTP auth (27235). Unlike relay auth it
authorizes an arbitrary HTTP request as the user — including destructive
NIP-96 blob deletes and NIP-86 relay-management admin calls — so its blast
radius is too broad to sign silently. Test pins the 42-vs-98 contrast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo
Signing a Lightning zap request moves no money — it only fetches an
invoice. The payment itself is the separately-gated value.payInvoice
capability, which prompts on every use regardless of policy. So adding
9734 to the reasonable set drops a redundant signature prompt while the
meaningful payment prompt stays.
Nutzaps (9321) remain excluded: publishing one *is* the payment, since
the event carries the spendable ecash proofs. Test pins the contrast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo
Add more additive, public, non-destructive event kinds to the REASONABLE
signer policy so common apps stop prompting for every action. New kinds:
16 (generic repost), 20 (picture post), 42 (public chat message),
1111 (NIP-22 comment), 9802 (highlight), and 30315 (user status) — all in
the same risk class as the original 1/6/7 set.
Kinds that can spend money, overwrite account config (profile, contacts,
relay/mute/bookmark lists), delete content, or leak private data still
prompt. Decryption also stays ASK.
Refactors reasonableDecision() to a documented REASONABLE_SIGN_KINDS set
backed by quartz KIND constants, and adds NostrSignerPermissionLedgerTest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMrHZfwN5tM4ecvdz7xigo
Adds a Signature field to Compose Settings (global UI settings, DataStore
persisted). When opening any text-based composer — new note, reply, quote,
poll, NIP-22 comment (reply/hashtag/geohash/url), or a new long-form
article — the signature is appended to the message with a blank line,
keeping the cursor at the start so the user types above it.
Drafts, forks, and version edits are skipped since their content already
carries (or deliberately omits) a signature, and an untouched
signature-only message is treated as blank so closing the composer never
auto-saves a junk draft.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vq4JQPB9m62nJ8Vdp7xVLN
relay.damus.io is being decommissioned, so remove it from every runtime
default/fallback relay set to stop the app and amy from wasting connection
slots on a dead host:
- commons Constants: remove `damus`; it dropped out of `bootstrapInbox`
(default NIP-65 inbox) and `eventFinderRelays` (default outbox/fallback),
both still carrying 6 healthy relays.
- ChessConfig: remove damus from CHESS_RELAYS / CHESS_RELAY_NAMES, leaving
the 3 relays the FETCH_TIMEOUT comment already assumes.
- desktop DefaultRelays: remove damus and the also-dead relay.snort.social.
- desktop FollowPacks DISCOVERY_RELAYS: remove damus.
- amy NipCommand SEARCH_RELAYS: swap damus for the NIP-50-capable nostr.wine.
Comments, @Preview sample data, and test fixtures that mention damus.io are
left untouched — they have no runtime effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ttcqa3V78bugGraGhtehj
Addresses the P0 items in the security review at
docs/plans/2026-07-01-privacy-lock-security-review.md.
## PBKDF2 iterations 100k → 600k (M1) via versioned hash format (M2)
- New PasswordHasher storage format: `v1$saltB64$hashB64` (600k
iterations, matches OWASP 2023 Password Storage Cheat Sheet for
PBKDF2-HMAC-SHA256).
- Legacy `saltB64$hashB64` (100k iterations) format still verifies
correctly — no user gets locked out by the bump.
- `hash()` always produces `v1$…`; users migrate to v1 opportunistically
when they Change or Set a new password.
- New `PasswordHasher.isLegacyFormat()` helper for callers that want
to force-migrate on next successful unlock.
- Verify cost goes from ~50ms → ~250ms on a modern laptop — well
within tolerable UX for a lock users open a handful of times per
session.
## Exponential backoff on failed unlock (M3)
- `PrivacyLockSettings` gains `failedUnlockAttempts: StateFlow<Int>`
and `lockedUntilEpochMs: StateFlow<Long?>`, both persisted via
java.util.prefs so a reboot cannot reset the backoff.
- `MessagesLockState.onFailedUnlockAttempt(nowMs)` implements the
schedule: no lockout for first 4 fails, then 30s / 60s / 120s /
300s (capped at 5 min).
- `MessagesLockState.onUnlockSuccess()` transparently clears the
attempt counter and any active lockout (also called from the
banner-enable path).
- DesktopLockScreen shows a countdown ("Try again in 27s") in the
supportingText, disables the password field and Unlock button
during lockout, ticks every 500ms via a LaunchedEffect.
- RemovePasswordDialog inherits the same protection — Settings can't
bypass the throttle by disabling the lock.
- 4 new unit tests cover threshold behavior, base trip, doubling +
cap, reset on success. All 13 tests green.
## Not in this commit
- L1/L2 (String/CharArray memory retention) — out-of-tree fix in
Compose; accepted per threat model.
- L3 (post-uninstall prefs) — release-notes item.
- M4 (Limitations copy update) — deferred; existing "does not
protect against filesystem access" line already covers.
Adds an inline banner at the top of the Desktop Messages deck column
that nudges users to enable the privacy lock. Fires only when
!lockEnabled && !firstRunCardSeen; dismissal is sticky across
restarts + lock enable/disable cycles.
- MessagesFirstRunBanner: AnimatedVisibility(expandVertically + fadeIn)
wrapper around a Surface + Row with a padlock icon, title, body,
and Enable / Not now buttons. Modeled on OfflineBanner.kt.
- SetPasswordDialog extracted from PrivacyLockSettingsScreen.kt into
a shared desktop/security/ file so the banner and the settings pane
both point at the same composable.
- MessagesLockState.onUnlockSuccess() relaxed to accept Disabled as a
valid previous state, so enabling from the banner keeps the user
Unlocked and doesn't flash the lock screen. New unit test covers
this path; all 9 tests green.
- DesktopMessagesScreen wraps its two-pane / compact layout in a
Column with the banner on top and a Box(weight(1f)) around the
panes so fillMaxSize propagates correctly.
Phase 5 (Desktop-only). Wraps the Messages deck column behind a
PBKDF2-hashed password gate; drops the Android-app slice.
- PrivacyLockSettings gains passwordHashed field + setter (salt$hash,
base64). Backed by java.util.prefs on desktop.
- PasswordHasher: PBKDF2-HmacSHA256, 100k iterations, 16-byte salt,
256-bit key, constant-time compare. Same primitive family as
SecureKeyStorage.
- DesktopMessagesLockGate: synchronous branch select in composition
(no LaunchedEffect guard) — closes the deep-link race per plan
§Security Hardening H1. Renders content when Disabled/Unlocked;
renders inline password TextField when Locked. Fires
MessagesLockState.onLeaveRoute() in DisposableEffect onDispose so
navigating away from the Messages column re-locks immediately.
- DesktopMessagesLockGate handles the "no password set" edge case
with a Disable-lock affordance.
- LocalPrivacyLockSettings CompositionLocal + LocalMessagesLockState
(from commons) both provided once at the App composition root in
Main.kt. Constructed with the existing windowScope so the state
holder's idle timer coroutines are lifecycle-scoped to the Window.
- DeckColumnContainer: DesktopMessagesScreen wrapped in
DesktopMessagesLockGate for the Messages column.
- Desktop PrivacyLockSettingsScreen: Column + Card layout matching
LocalRelaySettingsScreen (no Scaffold). Toggle, "Change password"
affordance with a full set/change dialog (old + new + confirm),
inactivity timer dropdown (1m / 5m / 15m / 1h / Never), redaction
level dropdown (Hidden / Full), honest limitations copy. Auto-opens
the set-password dialog if user toggles ON with no password set.
- Slotted into the existing Settings pane in Main.kt right after
LocalRelaySettings.
Damus-inspired content filter that collapses notes abusing `t` hashtag
tags into a compact reveal-on-click placeholder. Ships default ON with a
threshold of 5 (adjustable 1–20 in Settings → Content Filters, or off).
Scope
- Pure check (`HashtagSpamCheck`) + settings interface
(`HashtagSpamSettings`) live in `commons/moderation/`, callable by
Desktop, `amy` CLI, and (future) Android.
- JVM-backed `PreferencesHashtagSpamSettings` writes to the shared
`java.util.prefs` node `com/vitorpamplona/amethyst/filters`, so `amy`
and Desktop observe the same value automatically.
- `CollapsedSpamNote` placeholder in `commons/ui/note/` takes only
primitive scalars so Android can adopt it without touching commons.
- Desktop wraps every `NoteCard` call site (FeedNoteCard, QuotedNoteEmbed,
BookmarksScreen, 5 SearchResultsList sites) with a shared
`SpamCheckedNoteRender` helper. Thread root notes auto-expand via
`forceReveal=true`; replies still respect the filter.
Exemptions
- Long-form articles (kind 30023)
- Authors in the follow list plus self
- Repost wrappers check the inner event's tags via precomputed
`note.replyTo`, falling back to `containedPost()`
Search UX fixes bundled in
- Removed the `#hashtag` → "Direct lookup" card. `QueryParser` already
extracts `#xxx` into the query's hashtag filter, so typing `#bitcoin`
now goes straight to filtered results.
- Search-result rows now trigger metadata loading via
`subscriptionsCoordinator.loadMetadataBatched(authors)` and observe
each user's metadata flow via a new `rememberDisplayData` helper, so
display names + avatars refresh when kind-0 arrives from index
relays. Same helper reused in Bookmarks.
Tests + docs
- 19 unit tests (check × 10, displayed-event unwrap × 4, prefs × 5),
all green.
- Manual testing sheet with 16 scenarios at
`desktopApp/plans/2026-06-29-hashtag-spam-filter-manual-testing-sheet.md`.
- Plan at `docs/plans/2026-06-29-feat-desktop-hashtag-spam-filter-plan.md`.
- Cross-client desktop feature backlog reference at
`desktopApp/plans/_desktop-feature-backlog.md`.
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.
Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:
shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4
docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
Adds a Follow Packs experience to Amethyst Desktop:
- New "Discover" sidebar destination with featured-pack hero, hashtag chips
driven by NIP-12 `t` tags, a 3-up "From the pack" notes feed, and a
right-rail of mini pack thumbnails.
- New "Follow Packs" launchable column (App Drawer + Discover "Browse all")
with multi-field search across title, description, creator name/npub,
and `t` tags.
- Pack detail overlay (read-only) with per-member Follow/Unfollow buttons
that reflect the live kind-3 state, plus pack-level Follow all /
Unfollow all with a dedupe-aware confirm dialog ("Follow N new (M
already followed)").
- Bulk follow / unfollow batched into a single kind-3 publish via new
`FollowActions.buildUnfollowBatch` and `Kind3FollowListState.follow/
unfollow(users: List<User>)`. The mutating call sites are Mutex-
protected against concurrent races.
- naddr → 39089 references in notes render as a rich inline card with
avatar stack + Follow all CTA. Cache miss triggers a one-shot
subscription; empty / deleted packs render minimal states.
- Shuffle button rotates both the featured pack and the gallery,
excluding the last 5 shown.
- Pack image fields render via Coil `AsyncImage` with a deterministic
gradient fallback.
Protocol additions:
- Quartz: `FollowListEvent.hashtags()` convenience accessor.
Bug fixes wrapped into the feature:
- `DesktopLocalCache.consumeContactList` now also loads the event into
`addressableNotes` so `Kind3FollowListState.getFollowListEvent()`
returns the user's actual kind-3. Without this, every bulk follow
silently replaced (rather than appended to) the contact list.
- Added Material Symbols `Shuffle` codepoint and regenerated the
bundled subset font (still 432 KB).
Pressing Back on the "Connect to Nostr" first-connect dialog resolves to
AppConnectResult.Cancelled, which added the app's coordinate to an in-memory
`sessionCancelled` set. That set suppressed every future connect prompt for the
entire broker lifetime, so a later request — e.g. the user re-clicking "login"
in the in-app browser — was silently denied and the dialog never reappeared.
Because a Cancel persists nothing, the app also never showed up in Connected
Apps, leaving the user with nothing to clear to recover.
Replace the permanent suppression with a short, self-clearing cooldown
(`cancelledUntil` map): a Cancel suppresses re-prompts only briefly so the
burst of requests a page/napplet fires on load doesn't relaunch the dialog per
request, while a deliberate retry seconds later prompts again. The clock is
injectable for deterministic tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDcts4HzwSff4fy6oSVA6D
- Top-bar repo bookmark toggle now switches between BookmarkAdd (with +)
and Bookmark glyphs instead of two identical glyphs, so the icon
visibly changes shape (not just tint) when starred/unstarred.
- Add a thin HorizontalDivider after the ReactionsRow on the repo home.
- Code browser: opening/closing a file or changing folders swaps the
scrollable in place, landing the new view at the top with no scroll
delta, which left the disappearing top bar stranded at its hidden
offset over a blank band. Expose the scaffold bar state via
LocalDisappearingBarState and reset it to visible on each in-place view
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The commonMain GitRepositoryListState used Dispatchers.IO without importing
the multiplatform kotlinx.coroutines.IO extension, so it resolved to the
JVM-only member and broke the iOS native compile
(:commons:compileKotlinIosSimulatorArm64). Matches BookmarkListState.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Snapshot cache: a process-wide GitRepoSnapshotCache keyed by repo address.
The browser ViewModel serves an already-fetched default-branch snapshot
synchronously, so the stats render in share-to-image and don't re-fetch when
switching screens.
- Issues/PR screens: filter chips now live inside the disappearing top bar
(via the scaffold's belowBar slot) so they hide with it instead of leaving a
static black band; the feed uses normal content padding.
- New issue is now an extended FAB on the Issues screen.
- Top bar shows the repo description as a single-line subtitle under the name;
removed the duplicate description from the home body.
- Tighter spacing: home sections, code header rows (branch row / search /
breadcrumb).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The KMP lifecycle-viewmodel artifact used by commons doesn't expose the
create(Class<T>) ViewModelProvider.Factory override (only the desktop/JVM
target hit this), so the factory now lives in amethyst alongside the
viewModel() call, mirroring the NestViewModel pattern. The commons
ViewModel keeps only platform-agnostic state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The value-for-value split editor's state holder is pure snapshot state over
quartz types + a commons User — no Account, LocalCache, AccountViewModel, or
Android dependency — so per the commons architecture (state holders belong in
commons, CLI-safe where practical) it moves to commons.podcasts. A future
Desktop/iOS V4V editor can now drive the same state; the editor composable stays
platform-side (it needs AccountViewModel + user search).
This is the only podcast app-layer file that's free of amethyst-only
foundations: the rest of the podcast UI / ViewModels / feed filters /
subscriptions are coupled to AccountViewModel, LocalCache, Account, the
per-user subscription framework, or Android media/upload — the same foundations
every feature in the app shares, none of which live in commons — so they stay in
amethyst (as does, for the same reason, the analogous music composer). The
podcast protocol itself was already fully shared: all 50 quartz podcast files
live in commonMain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
Relocates the two app-agnostic pieces of the NIP-34 code browser into commons
so the desktop front end can reuse them verbatim:
- GitRepositoryBrowserViewModel (+ GitBrowseState) → commons jvmAndroid
nip34Git package. Pure StateFlow ViewModel over quartz's GitHttpClient;
no Android/AccountViewModel/INav dependency.
- CodeHighlighter → commons commonMain nip34Git/ui. Pulls the Apache-2.0
dev.snipme:highlights dependency into commons commonMain.
Amethyst composables now import both from commons. The screen-level
composables stay app-side, matching the commons convention that shared UI
never takes AccountViewModel/INav.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Adds NIP-51 (kind 10018) repository bookmarking with a star toggle in the
git repository top bar, backed by a new GitRepositoryListState in commons.
Removal rebuilds the public tag set and re-signs so encrypted private
bookmarks are preserved without decryption.
Adds a label-filter chip row and open/closed item counts to the Issues and
Patches & PRs status feeds. The active feed's distinct labels drive the
chips; selecting one filters the rendered list, and a stale selection is
dropped when switching status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Render the repository README in the first tab and add a Code tab that
browses the repo's file tree and renders source files (syntax-highlighted),
reading directly from the NIP-34 clone URL over the git smart-HTTP v2
protocol (works with GRASP/ngit bare servers as well as GitHub/GitLab).
quartz (jvmAndroid): a from-scratch git smart-HTTP v2 client — pkt-line
codec, packfile parser with OFS/REF delta resolution and SHA-1 oids,
tree/commit parsers, and a high-level browser that fetches a shallow
filter=blob:none snapshot (one request for the whole tree) and lazily
pulls file blobs on demand. Offline tests run against real captured
GitHub wire bytes plus a git-generated OFS-delta pack.
amethyst: README tab (rich markdown), Code tab (folders-first browser
with breadcrumb navigation + a file viewer that renders markdown or
syntax-highlighted source), a browser ViewModel, new UI strings, and a
Folder material symbol (font subset regenerated). Syntax highlighting
uses dev.snipme:highlights (Apache-2.0, permissive).
Tabs are now: README, Code, Overview, Issues, Patches & PRs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Extract resolveNappletMeta() to NappletManifestLookup.kt, replacing
three private copies of the same manifest lookup across
ConnectedAppsScreen, ConnectedAppDetailScreen, NappletPermissionsScreen,
and NappletSignerConsentActivity.
- Extract PolicyCard composable to PolicyCard.kt, shared between
ConnectedAppDetailScreen and RelayAuthSettingsScreen (was duplicated).
- Extract NappletCapability.symbol() to NappletCapabilityExt.kt, shared
between ConnectedAppDetailScreen and NappletPermissionsScreen.
- Drop what-comments on kind 1/6/7 lines in NostrSignerPermissionLedger.
- Reword TrustedRelayListState stateIn comment to note private-tag absence
on first boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- Cancel on first-connect dialog now suppresses re-prompting for the
rest of the session (sessionCancelled set under signerConsentLock)
instead of showing the dialog on every subsequent request.
- PARANOID signer policy no longer silently bulk-grants ALLOW_ALWAYS
for all capabilities; the capability ledger is left empty so each
capability prompts individually, matching user intent.
- NostrSignerOp.Decrypt default changed from ALLOW → ASK in
reasonableDecision(); the branch is currently unreachable
(toSignerOp() never produces Decrypt) but ASK is the safer default
if a decrypt request type is added in future.
- TrustedRelayListState seeds its StateFlow from the synchronously
available cached relay set, eliminating a startup window where
IF_IN_MY_LIST incorrectly denied auth to relays in the user's list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Remove the signer self-gating bypass that allowed external (Amber/NIP-55)
and remote (NIP-46) signers to skip Amethyst's per-napplet consent UI.
All signer types now go through Amethyst's consent dialogs first; the
external signer then adds its own approval on top (double-prompting).
This lets users differentiate signing requests by app inside the external
signer, since Amethyst itself is the requesting app.
Also expand the REASONABLE policy to auto-approve Encrypt and Decrypt
operations, matching the intent that common/private-key operations that
apps routinely need are pre-approved at the "reasonable" trust level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- Add AllowForSession and AllowUntil(expiresAt) signer grant types so users
can grant temporary access (session, 24h, 30d) from the consent dialog
- Track per-app lastUsed timestamp in NostrSignerPermissionStore and update
it on every granted signing operation
- Auto-expire timed grants: decide() clears expired op decisions before
returning, so no background sweep is needed
- Add NappletBroker.sessionAllows in-memory set for session grants (cleared
on broker destroy, never persisted)
- Implement full NIP-42 relay auth policy system:
- RelayAuthPolicy enum (ALWAYS / NEVER / IF_IN_MY_LIST) stored in
AccountSettings and persisted in LocalPreferences
- RelayAuthDecision (ALLOW / DENY) per-relay overrides in DataStore
- RelayAuthPermissionLedger combining global policy + per-relay overrides
- DataStoreRelayAuthPermissionStore writing to relay_auth.preferences_pb
- Wire relay auth into AuthCoordinator: subscribeLedger/unsubscribeLedger
lets each logged-in account contribute its own policy; signWithAllLoggedInUsers
now receives the relay URL so it can check the ledger before signing
- Update RelayAuthenticator (quartz) to pass relay URL in the signing lambda
- Add RelayAuthSubscription composable that subscribes both the account and
its ledger when a screen is active
- Add RelayAuthSettingsScreen: global policy radio picker + per-relay
override list with toggle and remove; reachable from Settings
- Add Route.RelayAuthSettings, AppNavigation wiring, and SettingsCatalog entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Implements per-app permission management for the internal nsec signer when
webapps/napplets/nsites connect via Amethyst's built-in key:
- Three trust levels on first connect (FULL_TRUST, REASONABLE, PARANOID)
with a UI dialog (NappletConnectActivity) matching the design spec
- Per-operation consent dialogs (NappletSignerConsentActivity) for
sign-kind/encrypt/decrypt with Allow once, Don't ask again, Deny options
- Per-app DataStore storage (DataStoreNostrSignerPermissionStore) using
SHA-256-hashed filenames so 1000s of apps don't bloat a single file
- NostrSignerPermissionLedger applies policy decisions: REASONABLE
auto-allows kinds 1/6/7; FULL_TRUST auto-allows all non-payment ops
- NappletBroker extended with first-connect gate and per-op signer gate,
serialized by a dedicated signerConsentLock mutex
- Permission management screen (NappletSignerPermissionsScreen) to review
and revoke stored per-app signer permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Render the browser "Discover" section as full-width rows (icon + name +
one-line description) instead of bare icon cells, matching the Recent
row layout which already carries a subtitle. Each suggestion now has a
short curated description (trimmed from the app's own meta description)
so unfamiliar apps explain themselves; tapping a row opens the app, and
a trailing star pins it to favorites.
Auto-pulling page <title>/description was rejected: many of these apps
are client-rendered SPAs that serve an empty <title>, and several titles
are long marketing strings — curated short names read better in the list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Grow the browser "Discover web apps" list to the full set of
browser-openable Nostr web apps from the nostrapps.com directory plus
several requested additions, and give each entry its own logo.
- Each suggestion now carries iconUrl set to the app's own declared
apple-touch-icon / icon (PNG or SVG, individually verified to return an
image), so the grid matches the favicon look of Favorites/Recent
without any third-party favicon service. Apps whose only icon is an ICO
(Coil has no ICO decoder) or that couldn't be resolved stay icon-less
and fall back to the globe glyph until their favicon is captured on
first visit.
- Added: nymchat, nostr.build, nostrcheck, zap.cooking, x21, divine.video,
brainstorm, zappix, plektos, zaptrax, zaplytics, podstr, ghostr, mutable,
metadata, plebsvszombies, blobbi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Add a "Discover web apps" section to the browser launcher home, shown
after the Recent block, with a hardcoded list of popular Nostr web apps
drawn from the nostrapps.com directory. Gives new users (whose Favorites
and Recent are empty) somewhere to start instead of a bare empty screen.
- New DefaultWebClients in commons (URL + label entries), grouped by
category; extensions/signer-only tools are excluded and every URL is a
confirmed canonical domain. No remote icons are loaded on the idle
screen — favicons are captured the normal way once a site is opened.
- Render the list via a new suggestedAppItems grid (long-press offers
"Add to favorites"); already-favorited apps are filtered out.
- FavoriteAppCell now takes a menu slot so the favorites grid and the
suggestions grid can offer different long-press actions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Continues moving genuinely platform-agnostic leaves out of the :amethyst app
module so they compile once in :commons instead of across all six app variants.
Moved (no Android coupling, no foundation deps):
- ui/layouts/DisappearingBarState, DisappearingBarNestedScroll, PaddingMerge
-> commons commonMain (com.vitorpamplona.amethyst.commons.ui.layouts)
- ui/components/UrlPreviewState
-> commons jvmAndroid (it references commons.preview.UrlInfoItem, which
lives in the jvmAndroid source set)
Consumers (incl. the existing DisappearingBar*Test unit tests, which stay in
:amethyst and now import from commons) updated to the new packages. No behavior
change.
Verified: :commons, :amethyst compilePlayDebugKotlin + compilePlayDebugUnitTest,
and :desktopApp:compileKotlin build clean; spotless applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
First Tier A slice of the UI/components extraction. Moves the genuinely
platform-agnostic leaf composables — those with zero :amethyst
dependencies and no Android coupling — from the app module into the
shared :commons KMP module (commonMain):
ClickableTexts, ForwardingPainter, GenericLoadable, GlowingCard,
LoadingAnimation, TranslationConfig, ZonedSwipeModifier (~616 LOC)
These now live under com.vitorpamplona.amethyst.commons.ui.components and
compile once in :commons (cacheable, incremental) instead of being part
of every one of the six :amethyst variant compilations (play/fdroid ×
debug/release/benchmark). That shrinks the app-module Kotlin compilation
unit — the root cause of the CI Kotlin-daemon OOM — rather than renting
headroom with heap flags.
Consumers updated to import from the new package; no behavior change.
Verified: :commons, :amethyst compile{Play,Fdroid}DebugKotlin, and
:desktopApp:compileKotlin all build clean; spotless applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
The consolidation passed `useProxy = true` for every brokered `resource.bytes`
fetch, forcing them through Tor whenever Tor was active — regardless of the
napplet/nSite's actual network mode. That overrides the user's explicit choice:
an nSite running in "open web" mode would still have its blob fetches tunneled,
inconsistent with how its own WebView page loads.
The authoritative per-applet preference already exists main-side in
NappletNetworkRegistry.useTor(coordinate) (locked napplets pinned to Tor;
nSites follow the persisted per-site toggle, which relaunches on change) — the
same source NappletLauncher reads to set the WebView proxy. Thread the calling
applet's coordinate through NappletResourceGateway.fetch so the broker can
resolve it, and pick the shared client with
getHttpClient(useProxy = NappletNetworkRegistry.useTor(coordinate)). This
mirrors the host's own `effectiveProxy = if (useTor) proxyPort else -1` exactly,
so a brokered fetch now routes like the applet's page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The browser/napplet strings (browser_address_hint, browser_console_title,
browser_console_title_short, browser_console_clear, napplet_untitled) were
moved to :commons, but their per-locale translations were left behind in
amethyst's values-*/strings.xml. With the default keys gone from amethyst,
lint flagged them as ExtraTranslation (80 errors across 16 locales).
Move the translations into commons/src/androidMain/res/values-*/strings.xml
so the default key and its translations live in the same module, preserving
the existing translation work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uza7sGxYPZtY43Ln2yH8FQ