From b980f3a9ba70c6698e7d26912b1ca524959428d1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:34:07 +0000 Subject: [PATCH] Fix NIP-07 login by waiting for extension injection Implemented `waitForNip07Extension` polling in `js/nostr/client.js` to address race conditions where `window.nostr` is not immediately available. Updated `js/nostr/nip07Permissions.js` with the polling logic and added unit tests. --- js/nostr/client.js | 7 ++++ js/nostr/nip07Permissions.js | 25 +++++++++++++ tests/nostr/nip07Permissions.test.js | 54 ++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/js/nostr/client.js b/js/nostr/client.js index b741d83a..6e87e9dc 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -139,6 +139,7 @@ import { requestEnablePermissions, runNip07WithRetry, writeStoredNip07Permissions, + waitForNip07Extension, } from "./nip07Permissions.js"; import { clearStoredSessionActor as clearStoredSessionActorEntry, @@ -3958,6 +3959,12 @@ export class NostrClient { */ async login(options = {}) { try { + try { + await waitForNip07Extension(); + } catch (waitError) { + devLogger.log("Timed out waiting for extension injection:", waitError); + } + const extension = window.nostr; if (!extension) { devLogger.log("No Nostr extension found"); diff --git a/js/nostr/nip07Permissions.js b/js/nostr/nip07Permissions.js index 7e5f39aa..61395fcf 100644 --- a/js/nostr/nip07Permissions.js +++ b/js/nostr/nip07Permissions.js @@ -310,3 +310,28 @@ export const __testExports = { clearStoredNip07Permissions, normalizePermissionMethod, }; + +export function waitForNip07Extension(timeoutMs = 1500) { + return new Promise((resolve, reject) => { + if (typeof window !== "undefined" && window.nostr) { + resolve(window.nostr); + return; + } + + if (typeof window === "undefined") { + reject(new Error("Not running in a browser environment.")); + return; + } + + const start = Date.now(); + const interval = setInterval(() => { + if (window.nostr) { + clearInterval(interval); + resolve(window.nostr); + } else if (Date.now() - start > timeoutMs) { + clearInterval(interval); + reject(new Error("Nostr extension not found within timeout.")); + } + }, 50); + }); +} diff --git a/tests/nostr/nip07Permissions.test.js b/tests/nostr/nip07Permissions.test.js index ad272181..2558505c 100644 --- a/tests/nostr/nip07Permissions.test.js +++ b/tests/nostr/nip07Permissions.test.js @@ -6,6 +6,7 @@ import { writeStoredNip07Permissions, clearStoredNip07Permissions, requestEnablePermissions, + waitForNip07Extension, } from "../../js/nostr/nip07Permissions.js"; test("writeStoredNip07Permissions normalizes and persists granted methods", () => { @@ -74,3 +75,56 @@ test("requestEnablePermissions reports unavailable extension", async () => { assert.strictEqual(result.ok, false); assert.strictEqual(result.error?.message, "extension-unavailable"); }); + +test("waitForNip07Extension resolves when extension is present", async () => { + const originalWindow = globalThis.window; + globalThis.window = { nostr: { dummy: true } }; + try { + const result = await waitForNip07Extension(); + assert.deepStrictEqual(result, { dummy: true }); + } finally { + if (originalWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } + } +}); + +test("waitForNip07Extension resolves when extension appears later", async () => { + const originalWindow = globalThis.window; + globalThis.window = {}; // No nostr yet + + setTimeout(() => { + globalThis.window.nostr = { later: true }; + }, 50); + + try { + const result = await waitForNip07Extension(500); + assert.deepStrictEqual(result, { later: true }); + } finally { + if (originalWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } + } +}); + +test("waitForNip07Extension rejects when extension never appears", async () => { + const originalWindow = globalThis.window; + globalThis.window = {}; // No nostr + + try { + await waitForNip07Extension(100); + assert.fail("Should have rejected"); + } catch (error) { + assert.strictEqual(error.message, "Nostr extension not found within timeout."); + } finally { + if (originalWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } + } +});