account-state
Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow),…
NostrSigner in Amethyst: NostrSignerInternal (local key), NostrSignerRemote (NIP-46 bunker), NostrSignerExternal (NIP-55 Android app). Use when signing/encrypting, adding a signer kind, login flows, or debugging sign timeouts.
Claude Code
$ npx -y skills add vitorpamplona/amethyst --skill auth-signers --agent claude-code
Codex
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a codex -y
Cursor
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a cursor -y
Gemini CLI
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a gemini-cli -y
GitHub Copilot
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a github-copilot -y
OpenCode
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a opencode -y
Windsurf
$ npx skills add vitorpamplona/amethyst --skill auth-signers -a windsurf -y
name: auth-signers description: NostrSigner in Amethyst: NostrSignerInternal (local key), NostrSignerRemote (NIP-46 bunker), NostrSignerExternal (NIP-55 Android app), Nip46SignerState (Amethyst as bunker). Use when signing/encrypting, adding a signer kind, login flows, testing a signer on an emulator, or debugging sign timeouts.
Every signed event and every NIP-04/NIP-44 operation goes through a `NostrSigner`. Feature code never knows which of the three kinds the user has.
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt`:
abstract class NostrSigner(pubKey: HexKey) {
open val pubKey: HexKey // open: NIP-46 resolves it from the bunker
abstract fun isWriteable(): Boolean
suspend fun <T : Event> sign(ev: EventTemplate<T>): T
abstract suspend fun <T : Event> sign(createdAt: Long, kind: Int, tags: Array<Array<String>>, content: String): T
abstract suspend fun nip04Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip04Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun nip44Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip44Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent
abstract suspend fun deriveKey(nonce: HexKey): HexKey
abstract suspend fun signPsbt(psbtHex: String): String // NIP-BC; may throw UnsupportedMethodException
abstract fun hasForegroundSupport(): Boolean
suspend fun decrypt(encryptedContent: String, fromPublicKey: HexKey): String // picks NIP-04 vs NIP-44
}Failures are thrown as `SignerExceptions` (`nip01Core/signers/SignerExceptions.kt`): `ReadOnlyException`, `UnauthorizedDecryptionException`, `NothingToDecrypt`, `AutomaticallyUnauthorizedException`, `ManuallyUnauthorizedException`, `TimedOutException`, `CouldNotPerformException`, `RunningOnBackgroundWithoutAutomaticPermissionException`, `SignerNotFoundException`, `UnsupportedMethodException`. Same folder: `NostrSignerSync` (blocking, for tests/scripts), `EventTemplate.kt`, `caches/` (`DecryptCache`).
| Signer | File | Notes | |---|---|---| | `NostrSignerInternal` | `quartz/.../nip01Core/signers/NostrSignerInternal.kt` | key in process memory; created/imported accounts | | `NostrSignerRemote` | `quartz/.../nip46RemoteSigner/signer/NostrSignerRemote.kt` | NIP-46 bunker over kind 24133; `references/nip46-remote-signer.md` | | `NostrSignerExternal` | `quartz/src/androidMain/.../nip55AndroidSigner/client/NostrSignerExternal.kt` | NIP-55 app (Amber…) via intents + content provider; Android only; `references/nip55-android-signer.md` |
Prefer an `Account` method (`commons/.../commons/model/Account.kt`) — e.g. `reactTo(...)`, `follow(user)` — which builds the template, signs with `account.signer`, updates local state and broadcasts (`sendMyPublicAndPrivateOutbox`, `sendAutomatic`). If none fits, build the template from the event class's `build(...)` and `account.signer.sign(template)`.
From a ViewModel, run signer work through `BaseAccountViewModel.launchSigner { … }` (`commons/.../commons/viewmodels/BaseAccountViewModel.kt`): it runs on IO and turns `SignerExceptions` into user notices, so don't add your own catch-and-toast. Never retry a sign yourself: a timeout usually means the user didn't answer in time.
is a fresh `NostrSignerInternal` transport key), then `suspend fun connect()`.
`ExternalSignerLogin.parseResult(data)` for the pubkey and package; build `NostrSignerExternal(pubKey, packageName, contentResolver)`.
The login form is shared: `commonsUI/.../commons/account/ui/login/` (`LoginScreen`, `LoginViewModel`, `RemoteSignerLogin`, `KeyTextField`, …) driving `commons/.../commons/account/AccountSessionManager.kt`. The Android signer-app button, `amethyst/.../ui/screen/loggedOff/login/ExternalSignerButton.kt`, reaches the form through `AppPlatform.ExternalSignerLoginButton` (`commonsUI/.../commons/ui/platform/AppPlatform.kt`, implemented in `amethyst/.../ui/platform/AndroidAppPlatform.kt`).
| Signer | Latency | Offline | Key location | UX | |---|---|---|---|---| | Internal | µs | yes | app memory | no prompts | | Remote (NIP-46) | 100 ms – seconds | no (bunker relay must be reachable) | bunker | occasional approvals | | External (NIP-55) | 100–500 ms | yes | other app | prompt per sign unless pre-approved |
the NIP-55 foreground path waits 30 s (`IntentRequestManager.foregroundApprovalTimeout`). Anything that stamps a time before asking to sign (NIP-98, NIP-42) can expire during the prompt — sign right before use.
`get_public_key` answer (or `bindUserPubkey(...)` from a saved account); until bound it is the transport key. Self-encryption keyed off the wrong one breaks private lists and drafts.
without pre-approved permissions gets `RunningOnBackgroundWithoutAutomaticPermissionException`.
`UnsupportedMethodException`.
Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow),…
amy CLI in cli/: adding an `amy <verb>` command, Main.kt dispatch, Output.emit/--json contract, exit codes 0/1/2/124, ~/.amy…
Android shim in `amethyst/`: MainActivity, AndroidManifest/intent filters, :napplet process, play/fdroid flavors, R8 +…
Shared Compose UI in `commonsUI/`: screens, components, the nav shell (`ScreenLayoutSpec`, `AppShellLayout`, Navigation 3 back…
Desktop JVM shim in desktopApp/: AmethystDesktop window, DesktopMenuBar, platform ports, OS keyring; :desktopApp:run, packaging.…
Translation audits of `strings.xml` locales in `commonsUI/.../composeResources` and `amethyst/src/main/res`: missing or…
Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP…