mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
fix(cli): nak-compatible bunker — percent-encoded URIs + get_relays
Close the NIP-46 interop gap against the real nak binary: - `Identity.fromBunkerUri` now URL-decodes the relay/secret params. nak emits `bunker://<pk>?relay=wss%3A%2F%2F…&secret=…`; without decoding, `amy login` of a nak bunker URI produced a broken relay and never connected. (This was the one real break — found by testing vs nak.) - `amy bunker` now percent-encodes the relay/secret in the URI it prints, matching nak's output format. - `amy bunker` implements `get_relays` (returns its relay set), so nak clients that probe it get a proper reply instead of an error. Verified end-to-end with `go install`-built nak over relay.damus.io, both directions: - `amy login bunker://` ⇄ `nak bunker`: amy signs, event authored by nak's key, signature valid. - `nak event --sec bunker://<amy>` ⇄ `amy bunker`: nak signs through amy, event authored by amy's key; amy logs connect→ok, sign_event→ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
This commit is contained in:
+3
-1
@@ -223,7 +223,9 @@ Two amy processes can talk: one **hosts** a bunker with its local key; the other
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `amy bunker [--relay URL[,URL…]] [--secret S] [--timeout SECS]` | Run a NIP-46 remote signer for the active local-key account. Prints a `bunker://…` URI, then services sign / nip04 / nip44 / get_public_key / ping requests until interrupted (or `--timeout`). |
|
||||
| `amy login bunker://PUBKEY?relay=…&secret=…` | Log in through a bunker. Mints a local transport keypair; the account then acts as PUBKEY and every signing/encryption call is delegated to the remote signer. |
|
||||
| `amy login bunker://PUBKEY?relay=…&secret=…` | Log in through a bunker. Mints a local transport keypair; the account then acts as PUBKEY and every signing/encryption call is delegated to the remote signer. Percent-encoded relay params are decoded. |
|
||||
|
||||
Interop-tested against the real [`nak`](https://github.com/fiatjaf/nak) binary, both directions: `amy login bunker://` ⇄ `nak bunker`, and `nak event --sec bunker://` ⇄ `amy bunker`. Supports `connect` (secret-checked), `get_public_key`, `get_relays`, `sign_event`, `nip04_encrypt/decrypt`, `nip44_encrypt/decrypt`, `ping`. The `nostrconnect://` reverse flow and `auth_url` challenges are not implemented.
|
||||
|
||||
Example (two terminals, shared `$HOME`):
|
||||
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ vs streaming `subscribe`). Stateless verbs run with no account or network.
|
||||
| `sync` | `amy sync` | ✅ | NIP-77 Negentropy reconcile with the local store (down/up/both). |
|
||||
| `git` | `amy git` | ✅ in part | NIP-34 repo announce/list/show/issue. clone/push (packfile transport) out of scope. |
|
||||
| `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. |
|
||||
| `bunker` | `amy bunker` + `amy login bunker://` | ✅ | NIP-46 remote signer (server) + bunker login (client). Two amy processes interop. |
|
||||
| `bunker` | `amy bunker` + `amy login bunker://` | ✅ | NIP-46 remote signer (server) + bunker login (client). Interop-verified vs real `nak` both directions; connect/get_public_key/get_relays/sign/nip04/nip44/ping. `nostrconnect://` + `auth_url` still pending. |
|
||||
| `serve` / `admin` / `wallet` / `mcp` / `fs` / `spell` | — | 🆕 (tier 2/3) | larger/niche; some pull new deps. |
|
||||
|
||||
**Tier 1 status:** shipped — `decode`, `encode`, `verify`, `key`, `event`,
|
||||
|
||||
@@ -92,9 +92,12 @@ data class Identity(
|
||||
parts.getOrNull(1)?.split("&")?.forEach { param ->
|
||||
val kv = param.split("=", limit = 2)
|
||||
if (kv.size < 2) return@forEach
|
||||
// Relay/secret params are percent-encoded by spec-compliant
|
||||
// emitters (nak does: `relay=wss%3A%2F%2F…`), so decode them.
|
||||
val value = java.net.URLDecoder.decode(kv[1], "UTF-8")
|
||||
when (kv[0]) {
|
||||
"relay" -> relays.add(kv[1])
|
||||
"secret" -> secret = kv[1]
|
||||
"relay" -> relays.add(value)
|
||||
"secret" -> secret = value
|
||||
}
|
||||
}
|
||||
require(relays.isNotEmpty()) { "bunker uri must carry at least one relay" }
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt
|
||||
@@ -46,9 +47,11 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.ReadWrite
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@@ -93,11 +96,13 @@ object BunkerCommand {
|
||||
val secret = args.flag("secret") ?: KeyPair().privKey!!.toHexKey().take(32)
|
||||
val self = ctx.identity.pubKeyHex
|
||||
|
||||
// Percent-encode params (spec/nak convention: relay=wss%3A%2F%2F…).
|
||||
val enc = { s: String -> java.net.URLEncoder.encode(s, "UTF-8") }
|
||||
val uri =
|
||||
buildString {
|
||||
append("bunker://").append(self)
|
||||
append("?").append(relays.joinToString("&") { "relay=${it.url}" })
|
||||
append("&secret=").append(secret)
|
||||
append("?").append(relays.joinToString("&") { "relay=${enc(it.url)}" })
|
||||
append("&secret=").append(enc(secret))
|
||||
}
|
||||
Output.emit(
|
||||
mapOf(
|
||||
@@ -165,6 +170,7 @@ object BunkerCommand {
|
||||
BunkerResponseError(request.id, "invalid secret")
|
||||
}
|
||||
is BunkerRequestGetPublicKey -> BunkerResponsePublicKey(request.id, signer.pubKey)
|
||||
is BunkerRequestGetRelays -> BunkerResponseGetRelays(request.id, relays.associate { it.url to ReadWrite(read = true, write = true) })
|
||||
is BunkerRequestPing -> BunkerResponsePong(request.id)
|
||||
is BunkerRequestSign -> {
|
||||
val signed = signer.sign<Event>(request.event.createdAt, request.event.kind, request.event.tags, request.event.content)
|
||||
|
||||
Reference in New Issue
Block a user