From 7cb603b652d72f15cee1a42c0fa7ae6798385cf4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:19:47 +0000 Subject: [PATCH] fix(nostr): prevent nip-07 fallback from overwriting concurrent login - Added a check in `ensureActiveSignerForPubkey` to verify if a valid signer (e.g. NIP-46) was registered during the extension wait period. - Modified `nip07Permissions.js` to respect global timeout overrides for structured permission requests, fixing flaky tests. - Added regression test `tests/nostr-signer-race.test.mjs` to simulate the race condition. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/nostr/client.js | 6 +++ js/nostr/nip07Permissions.js | 2 +- tests/nostr-signer-race.test.mjs | 65 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/nostr-signer-race.test.mjs diff --git a/js/nostr/client.js b/js/nostr/client.js index 0ff3369d..56891a37 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -2642,6 +2642,12 @@ export class NostrClient { } } + // Check if a signer appeared while we were waiting (e.g. NIP-46 or concurrent NIP-07 logic) + const recheckedSigner = resolveActiveSigner(normalizedPubkey); + if (recheckedSigner && typeof recheckedSigner.signEvent === "function") { + return recheckedSigner; + } + if (!extension) { return existingSigner; } diff --git a/js/nostr/nip07Permissions.js b/js/nostr/nip07Permissions.js index 4cbc682d..18d719f7 100644 --- a/js/nostr/nip07Permissions.js +++ b/js/nostr/nip07Permissions.js @@ -275,7 +275,7 @@ export async function requestEnablePermissions( // If we fall back to standard enable() (null), allow the full user interaction time. const variantTimeoutOverrides = options ? { - timeoutMs: 3000, + timeoutMs: Math.min(3000, getEnableVariantTimeoutMs()), retryMultiplier: 1, } : { diff --git a/tests/nostr-signer-race.test.mjs b/tests/nostr-signer-race.test.mjs new file mode 100644 index 00000000..3cb34217 --- /dev/null +++ b/tests/nostr-signer-race.test.mjs @@ -0,0 +1,65 @@ + +import "./test-helpers/setup-localstorage.mjs"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { NostrClient, setActiveSigner, getActiveSigner } from "../js/nostr/client.js"; + +const HEX_PUBKEY = "a".repeat(64); + +test("ensureActiveSignerForPubkey prefers concurrent login over late extension injection", async () => { + // 1. Setup Environment + const originalWindow = global.window; + global.window = {}; + + try { + const client = new NostrClient(); + client.pubkey = HEX_PUBKEY; + // Pre-condition: Cache permissions to trigger the waitForNip07Extension logic + client.extensionPermissionCache.add("sign_event"); + + // Mock a NIP-46 signer that might be set during the race + const mockNip46Signer = { + type: "nip46", + pubkey: HEX_PUBKEY, + signEvent: async () => ({ id: "mock-signed" }), + nip04: { encrypt: async () => "enc", decrypt: async () => "dec" } + }; + + // Mock the NIP-07 extension that appears late + const mockExtension = { + getPublicKey: async () => HEX_PUBKEY, + signEvent: async () => ({ id: "ext-signed" }), + nip04: { encrypt: async () => "ext-enc", decrypt: async () => "ext-dec" } + }; + + // 2. Start the race + // Calling ensureActiveSignerForPubkey while window.nostr is missing but permissions exist + // causes it to enter waitForNip07Extension loop (polling). + const ensurePromise = client.ensureActiveSignerForPubkey(HEX_PUBKEY); + + // 3. Simulate Concurrent Login (e.g. NIP-46 restore completes) + // We wait a small amount to ensure ensureActiveSignerForPubkey has started polling + await new Promise(resolve => setTimeout(resolve, 50)); + + setActiveSigner(mockNip46Signer); + + // 4. Simulate Extension Loading late + global.window.nostr = mockExtension; + + // 5. Await result + const resultSigner = await ensurePromise; + + // 6. Assertions + const finalActiveSigner = getActiveSigner(); + + // We expect the NIP-46 signer to be preserved and not overwritten by the NIP-07 fallback + assert.equal(finalActiveSigner.type, "nip46", "Race condition! NIP-46 signer was overwritten by NIP-07 fallback."); + assert.equal(resultSigner.type, "nip46", "Returned signer should be the NIP-46 one."); + } finally { + if (originalWindow) { + global.window = originalWindow; + } else { + delete global.window; + } + } +});