fix(nip46): accept secret-echo connect response and send remote-signer-pubkey as first param per NIP-46#390
Open
DocNR wants to merge 2 commits into
Conversation
…r-pubkey as first param per NIP-46 NDKNip46Signer.blockUntilReady has two NIP-46 spec violations on the bunker:// path that prevent connecting to spec-compliant signers (present in every released NDK from 2.12.2 through 3.0.3): 1. The first parameter sent to `connect` is the user-pubkey instead of the remote-signer-pubkey. For bunker:// URIs without `?pubkey=` (the typical single-user signer case, e.g. Clave at https://github.com/DocNR/clave), this means an empty string is sent. Per NIP-46: connect: [<remote-signer-pubkey>, <optional_secret>, <optional_requested_perms>] Fall through to `bunkerPubkey` (the URI hostname, which is by definition the remote-signer-pubkey) when `userPubkey` is null. 2. The response handler only accepts `response.result === "ack"`. Per NIP-46 spec, valid `connect` responses are: "ack" OR <required-secret-value> Spec-compliant signers like Clave (LightSigner.swift) and any signer matching nostr-tools' BunkerSigner.fromURI semantics return the URI secret echoed back instead of "ack". Accept both forms. Also: when rejecting on an unexpected result, wrap in a real `Error` instead of passing `response.error` (which is undefined when the bunker returns a successful-shaped response that we don't recognize). This used to throw `TypeError: Cannot read properties of undefined` in downstream catch handlers — now they get a useful message. NDK 3.0.3 already does the right thing in `blockUntilReadyNostrConnect` (checks `response.result === this.nostrConnectSecret` at line 287); this aligns the bunker:// path with the spec and with that nostrconnect implementation. Adds three regression tests covering: - The secret-echoed-back response path (Clave-style) - The first-param-is-bunkerPubkey behavior when no userPubkey - The Error-not-undefined rejection on unexpected responses Real-world failure: stacker.news's NIP-46 bunker login hangs forever when paired with a Clave bunker URI because NDK rejects with `undefined`, and stackernews's catch handler does `e.message || e.toString()` which throws on undefined. A companion stacker.news PR extends their existing NDKNip46SignerURLPatch with the same fix; this upstream PR lets that override be deleted on next NDK bump.
Review feedback: the previous `[this.userPubkey || this.bunkerPubkey || ""]` still sent the user pubkey as connect.params[0] whenever the bunker URI carried `?pubkey=<user-pubkey>` (NDK parses that into userPubkey). Per NIP-46, connect.params[0] is the remote-signer-pubkey — always bunkerPubkey (the URI host). For multi-user signers, `?pubkey=` is the user identity the client wants to act as, not the connect target; it is resolved separately via the get_public_key call after the handshake (getPublicKey() returns the cached userPubkey early when it is already set, so this is a no-op cost). Sending userPubkey breaks signers that validate connect.params[0] against their own signer pubkey. nostr-tools' BunkerSigner sends `this.bp.pubkey` (the bunker pubkey) likewise. Adds a regression test with distinct bunkerPubkey and userPubkey asserting params[0] === bunkerPubkey, alongside the existing no-userPubkey test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NDKNip46Signer.blockUntilReadyhas two NIP-46 spec violations on thebunker://path that prevent connecting to spec-compliant signers (e.g. Clave, nsec.app's bunker, anything matchingnostr-toolsBunkerSigner.fromURIsemantics). Both bugs are present in every released NDK from 2.12.x through 3.0.3.Per the NIP-46 spec:
Bug 1 — first connect param is the user-pubkey instead of the remote-signer-pubkey
Line 347 sends
[this.userPubkey ?? ""]. Forbunker://URIs without?pubkey=(the typical single-user signer case),userPubkeyis null → an empty string lands as the first param. Per spec the first param is the remote-signer-pubkey, which for abunker://URI is the URI hostname (=bunkerPubkey).Fixed by falling through to
bunkerPubkeywhenuserPubkeyis null.Bug 2 — only
"ack"is accepted as a valid connect responseLine 354 only accepts
response.result === "ack". Per spec,"ack"OR the URI secret echoed back are both valid. Spec-compliant signers like Clave and any signer matchingnostr-toolsBunkerSigner.fromURIsemantics return the secret-echo form. NDK rejects withresponse.errorwhich isundefined(no error in the response) — leaving downstream catch handlers to throwTypeError: Cannot read properties of undefined (reading 'message').Fixed by accepting
response.result === "ack" || response.result === this.secret.Drive-by: rejection is now wrapped in a real
Errorso downstream handlers get a useful message instead ofundefined.Note:
blockUntilReadyNostrConnectalready does this rightLine 287 of the
nostrconnect://flow already matchesresponse.result === this.nostrConnectSecret. This PR aligns thebunker://path with that and with the spec.Real-world failure
stacker.news's NIP-46 bunker login hangs indefinitely when paired with a Clave bunker URI for exactly these reasons. Companion PR (stackernews/stacker.news#2947) extends their existing
NDKNip46SignerURLPatchworkaround with the same fix; once this PR ships, that workaround can be deleted on next NDK bump.Tests
3 new tests in
core/src/signers/nip46/index.test.ts(following the existing mock pattern):blockUntilReady resolves user on URI secret echoed back (NIP-46 spec)— covers Bug 2blockUntilReady sends bunkerPubkey as first connect param when no userPubkey— covers Bug 1 (URI without?pubkey=)blockUntilReady sends bunkerPubkey (not userPubkey) as first connect param when the URI carries ?pubkey=— covers Bug 1 for multi-user signer URIs (added after review feedback — the earlier fix still sentuserPubkeyasparams[0]whenever the URI carried?pubkey=)blockUntilReady rejects with a real Error (not undefined) for unexpected connect responses— covers the drive-byManually verified the fix against:
nak serverelay → resolvesrelay.powr.build→ resolves in ~2sTest plan
bunworkspace; I don't havebunavailable, andnpm installinsidecore/doesn't reproduce the workspace layout (vitestisn't hoisted intocore/node_modules). The 4 new tests are written to the exact pattern of the existingblockUntilReady resolves user on acktest — samecreateMockRpc()harness, samemockImplementationcallback shape. A maintainer runningbun testis the validation gate here; flagging explicitly rather than claiming a green run I didn't do.nak serverelay and against a real Clave bunker URI from an iOS TestFlight build onrelay.powr.build(resolves in ~2s). The mock-bunker harness drives the realNDKNip46Signercode path, not a reimplementation.🤖 Generated with Claude Code