Files
amethyst/.claude/skills/auth-signers/references/nip55-android-signer.md
Claude 8bd5063e10 refactor: consume Quartz from its own repository
Quartz now lives in github.com/vitorpamplona/quartz, split into
quartz-core, quartz-social, quartz-relay-client and the other quartz-*
libraries, with history kept. Amethyst depends on the `quartz` umbrella
artifact at a released version (1.20.0) instead of the :quartz project.

- Remove the quartz/ module, jitpack.yml and the Quartz CI, release and
  pre-push steps; tools/unicode-nfkc moved with it.
- Point every module at libs.quartz.
- Move the device-test fixtures the benchmark and relayBench still read
  to testdata/.
- NostrConnectLoginUseCase parses bunker messages with BunkerJson, which
  now owns NIP-46 JSON.
- Repoint docs and skills at the Quartz repository; the nostr-expert
  scripts read a Quartz checkout (QUARTZ_REPO, default ../quartz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZR5ZBfhFzRiXubJc5Evyv
2026-10-15 02:03:53 +00:00

2.6 KiB

NIP-55 Android external signer

Another Android app (Amber, …) holds the key; Amethyst asks it through Intents (foreground, with UI) or its ContentProvider (background, pre-approved). Android only.

Layout (nip55-android-signer/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/)

  • api/CommandType.kt — sign_event, nip04_encrypt/decrypt, nip44_encrypt/decrypt, get_public_key, decrypt_zap_event, derive_key, sign_psbt.
  • api/foreground/ — IntentRequestManager (id → channel, launches the intent, waits foregroundApprovalTimeout = 30 s) and intents/requests|responses|results/ per command.
  • api/background/queries/ — one ContentResolver query per command (SignQuery, Nip44EncryptQuery, LoginQuery, …).
  • api/permission/ — Permission + its serializers (the permissions requested at login).
  • client/ — NostrSignerExternal(pubKey, packageName, contentResolver) (the NostrSigner), ExternalSignerLogin (createIntent(permissions, packageName), parseResult(intent)), IActivityLauncher (registerForegroundLauncher, unregisterForegroundLauncher, newResponse, hasForegroundActivity), IsExternalSignerInstalled.kt (isExternalSignerInstalled(context), getExternalSignersInstalled(context)), handlers/BackgroundRequestHandler + ForegroundRequestHandler.
  • SignString.kt, JsonMapperNip55.kt.

Flow

  1. Login: ExternalSignerLogin.createIntent(...) → the signer app returns pubkey + package → parseResult(data) → NostrSignerExternal(pubKey, packageName, contentResolver).
  2. Each call tries the background query first; if the signer has not pre-approved that command, it falls back to a foreground intent through the registered launcher.
  3. No registered Activity → SignerExceptions.RunningOnBackgroundWithoutAutomaticPermissionException; no answer in 30 s → TimedOutException.

The Android entry point is amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt.

Gotchas

  • Without "always allow", every reaction or zap is an Activity round-trip — bad UX; ask for the permissions at login.
  • Desktop has no NIP-55; it uses NostrSignerInternal or NostrSignerRemote.
  • Tests, in the Quartz repository: nip55-android-signer/src/androidHostTest (JVM host) and quartz/src/androidDeviceTest/.../nip55AndroidSigner/ in its umbrella module (device/emulator only).