fix(cli): resolve NIP-46 bunker identity via get_public_key

`amy login bunker://<pubkey>?…` 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) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-07-17 16:56:07 -04:00
co-authored by Claude Opus 4.8
parent 3d99f10058
commit 28e63e3d92
@@ -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,