Files
amethyst/.claude/skills/auth-signers/references/nip46-remote-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.9 KiB

NIP-46 remote signer (bunker)

A bunker holds the user's key and signs on request. Amethyst talks to it with kind-24133 NostrConnectEvents, NIP-44-encrypted between a local transport keypair and the bunker's pubkey.

Layout (nip46-remote-signer/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/)

  • BunkerRequest*.kt — Connect, GetPublicKey, GetRelays, Ping, Sign, Nip04Encrypt/Decrypt, Nip44Encrypt/Decrypt, Invalid; BunkerRequestParser.kt.
  • BunkerResponse*.kt — Ack, Event, Encrypt, Decrypt, PublicKey, GetRelays, Pong, Error.
  • NostrConnectEvent.kt (kind 24133), NostrConnectURI.kt, BunkerMessage.kt, kotlinSerialization/.
  • signer/ — the client side: NostrSignerRemote (the NostrSigner), RemoteSignerManager (request/response correlation), per-method *Response parsers, SignerResult.
  • server/ — the bunker side Quartz can also run: NostrConnectSignerService, BunkerRequestProcessor, Nip46RequestAuthorizer.

Connection

bunker://<bunker-pubkey>?relay=wss://…&secret=…
  -> NostrSignerRemote.fromBunkerUri(uri, transportSigner: NostrSignerInternal, client)
  -> remote.connect()            // BunkerRequestConnect(remoteKey, permissions, secret, clientMetadata)
  -> remote.getPublicKey()       // binds the user identity

fromBunkerUri takes optional permissions and clientMetadata; the constructor also takes onAuthUrl for NIP-46 auth_url challenges (surface the URL; the request keeps waiting on the same id).

Identity

The constructor's signer is the transport key, not the user. NostrSignerRemote.pubKey returns the identity resolved by getPublicKey() or set with bindUserPubkey(...) (persisted accounts, the CLI's stored identity), and falls back to the transport key until then. Anything that encrypts to self or checks authorship must use pubKey after binding. Transport plumbing (response p filter, request addressing) uses signer.pubKey/remotePubkey on purpose.

Request lifecycle (RemoteSignerManager.launchWaitAndParse)

Each call creates a request with a random id, publishes it to the bunker relays (client.publish(event, relayList)), parks a channel in a LargeCache keyed by id, and waits up to timeout (65 s). No answer → one retry with a fresh id after 2 s → then SignerResult.RequestAddressed.TimedOut, which NostrSignerRemote throws as SignerExceptions.TimedOutException. Concurrent requests never interleave because responses route by id (newResponse).

Gotchas

  • Show progress for long waits; approval can take tens of seconds.
  • Only Nostr relays carry the traffic — a feature that can sign offline cannot with a bunker.
  • signPsbt is not implemented for remote bunkers (UnsupportedMethodException).