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

56 lines
2.9 KiB
Markdown

# NIP-46 remote signer (bunker)
A bunker holds the user's key and signs on request. Amethyst talks to it with kind-24133
`NostrConnectEvent`s, NIP-44-encrypted between a local **transport** keypair and the
bunker's pubkey.
## Layout ([nip46-remote-signer/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/](https://github.com/vitorpamplona/quartz/tree/main/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`).