From 28e63e3d920447b820df8c3f650742cb402e8b5e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 17 Jul 2026 16:56:07 -0400 Subject: [PATCH] fix(cli): resolve NIP-46 bunker identity via get_public_key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `amy login bunker://?…` was persisting the pubkey embedded in the bunker URI as the account identity. That pubkey is the remote-signer / connection key (Amber, nsec.app, nak all mint a dedicated one), not the user's identity key — so every "my events" fetch queried the wrong author (no kind-10002/10050/13302 found, empty timelines). After saving a provisional identity, connect the bunker and call the NIP-46 `get_public_key` RPC (already implemented on NostrSignerRemote), persisting the returned user pubkey as the account identity while keeping the bunker's remote key in Identity.bunker for RPC addressing. Best-effort: falls back to the URI pubkey if the bunker can't answer. Mirrors the app's NostrConnectLoginUseCase, which already stores the verified pubkey. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../amethyst/cli/commands/LoginCommand.kt | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/LoginCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/LoginCommand.kt index 7e2f3bce1b..27bc1d274c 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/LoginCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/LoginCommand.kt @@ -21,13 +21,17 @@ package com.vitorpamplona.amethyst.cli.commands import com.vitorpamplona.amethyst.cli.Args +import com.vitorpamplona.amethyst.cli.Context import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Identity import com.vitorpamplona.amethyst.cli.Output +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import okhttp3.OkHttpClient @@ -73,19 +77,53 @@ object LoginCommand { ) dataDir.saveIdentity(identity) + + // For a bunker, the pubkey in the URI is the REMOTE SIGNER's key, which for many signer apps + // (Amber, nsec.app) is a per-connection key distinct from the user's identity key. Resolve the + // real identity via the NIP-46 get_public_key RPC and persist THAT (the bunker's remote key is + // kept in Identity.bunker for transport addressing). Best-effort: if the bunker can't answer, + // fall back to the URI pubkey so login still succeeds. + val account = if (identity.bunker != null) resolveBunkerIdentity(dataDir, identity) else identity + Output.emit( mapOf( - "npub" to identity.npub, - "hex" to identity.pubKeyHex, - "read_only" to !identity.canSign, - "signer" to if (identity.bunker != null) "bunker" else "local", - "bunker_relays" to identity.bunker?.relays, + "npub" to account.npub, + "hex" to account.pubKeyHex, + "read_only" to !account.canSign, + "signer" to if (account.bunker != null) "bunker" else "local", + "bunker_relays" to account.bunker?.relays, "data_dir" to dataDir.root.absolutePath, ), ) return 0 } + /** + * Connect the freshly-saved bunker and ask it (NIP-46 `get_public_key`) for the user's real + * identity pubkey, re-persisting the [Identity] when it differs from the bunker's transport key. + * Returns the corrected identity, or [provisional] unchanged if the RPC fails. + */ + private suspend fun resolveBunkerIdentity( + dataDir: DataDir, + provisional: Identity, + ): Identity = + try { + Context.open(dataDir).use { ctx -> + ctx.prepare() + val real = (ctx.signer as NostrSignerRemote).getPublicKey().lowercase() + if (real == provisional.pubKeyHex.lowercase()) { + provisional + } else { + val corrected = provisional.copy(pubKeyHex = real, npub = real.hexToByteArray().toNpub()) + dataDir.saveIdentity(corrected) + corrected + } + } + } catch (e: Exception) { + System.err.println("[nip46] could not resolve identity via get_public_key (${e.message}); using the bunker URI pubkey") + provisional + } + private suspend fun resolveIdentity( key: String, args: Args,