When fetching profiles, first fetch the user's relay list (kind 10002) from the profile relays, save the write relays locally per account, and then fetch the profile metadata (kind 0) from the default profile relays plus the saved user relays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9rbXR3WuwKEbzBdypYFUV
5.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & development commands
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build (requires signing keystore)
./gradlew ktlintCheck # lint check (also runs on every git commit via pre-commit hook)
./gradlew ktlintFormat # auto-fix lint issues
./gradlew test --no-daemon # unit tests (also runs on every git push via pre-push hook)
./build.sh # builds both offline and free release variants to ~/release/
Git hooks are auto-installed via the root build.gradle.kts preBuild task — no manual setup needed.
Build flavors
| Flavor | Purpose |
|---|---|
free (default) |
Online variant with full networking (OkHttp, Coil, relay connectivity) |
offline |
No network stack; use BuildFlavorChecker.isOfflineFlavor() to guard network code |
Architecture
Request ingestion — three paths
External apps and relays reach the signer through three distinct paths:
nostrsigner://Intent →SignerActivity→ parsed byIntentUtils→ shown as bottom sheet- ContentProvider IPC →
SignerProvider→ synchronous signing viarunBlocking - NIP-46 relay events (kind 24133) →
NotificationSubscription→EventNotificationConsumer→BunkerRequestUtils
All three paths converge on Account.sign() / encrypt/decrypt methods backed by NostrSignerInternal.
Global state — Amber.kt
Amber is the Application class and acts as the DI container. Key singletons it owns:
applicationIOScope—CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler), used for all background workclient: NostrClient— the Quartz Nostr relay clientnotificationSubscription— keeps the NIP-46 filter alive in the backgroundprofileSubscription— per-account, throttled one-shot metadata fetch; first fetches the user's NIP-65 relay list (kind 10002) and saves it locally, then fetches the metadata (kind 0) from the default profile relays plus the saved user relays; started/stopped by the composables that display each account viaProfileSubscriptionEffect(not app-wide)isStartingAppState: MutableStateFlow<Boolean>— set totrueduringrunMigrations(); code that must wait for startup callsisStartingAppState.first { !it }settings.killSwitch— when true, all relays are disconnected; checked before every relay operation
Per-account isolation
Every npub gets its own:
SharedPreferencesfile (prefs_${npub})AppDatabase(amber_db_${npub}) — apps + permissionsLogDatabase— operation logsHistoryDatabase— request history
All databases are lazy-loaded and cached in ConcurrentHashMaps in Amber. Account data (including decrypted keys) is loaded via LocalPreferences.loadFromEncryptedStorage() and cached in LargeCache.
Permission system
Permissions are stored in ApplicationEntity + ApplicationPermissionsEntity (Room). Each permission entry has:
rememberType— auto-accept, auto-reject, or always-askacceptUntil/rejectUntil— time-bound grantskind— event-kind-specific rules
Before showing the approval UI, all three ingestion paths query the database; if a matching auto-accept rule exists, signing proceeds silently.
Biometric / PIN lock is UI-only
The biometric/PIN prompt (useAuth / usePin in AmberSettings, configured in SecurityScreen) is only an app-launch UI gate, not a signing gate. It is rendered by BiometricAuthScreen, which is invoked exclusively from the two UI entry points — MainActivity and SignerActivity — to unlock the app's screens before any approval bottom sheet is shown.
It does not protect the signing operations themselves:
- ContentProvider IPC (
SignerProvider) and NIP-46 relay events (EventNotificationConsumer→BunkerRequestUtils) never touchBiometricAuthScreen,Biometrics,useAuth, orusePin. They sign in the background based purely on the permission database. - When an auto-accept rule matches, all three paths sign silently without triggering the biometric/PIN prompt — including
nostrsigner://intents, which auto-finish before the UI is interacted with.
In other words, the lock controls who can open and navigate the app UI; it does not stand between a request and Account.sign(). Authorization for automatic signing is governed solely by the permission system (rememberType / acceptUntil / kind).
Key files
| File | Purpose |
|---|---|
Amber.kt |
Application singleton, Nostr client, relay connectivity |
LocalPreferences.kt |
Account/settings persistence, encrypted key storage |
IntentUtils.kt |
Parses nostrsigner:// URIs, creates IntentData objects |
SignerProvider.kt |
ContentProvider IPC signing |
BunkerRequestUtils.kt |
NIP-46 protocol handling, relay responses |
AccountStateViewModel.kt |
Auth state, account switching, toast notifications |
ConnectivityService.kt |
Foreground service, network monitoring, relay reconnection |
BiometricAuthScreen.kt |
UI-only app-launch lock (biometric/PIN); not a signing gate |
Biometrics.kt |
Wraps BiometricPrompt / keyguard credential prompt |
SecurityScreen.kt |
Toggles useAuth / usePin and the re-prompt interval |