diff --git a/js/nostr/client.js b/js/nostr/client.js index 96cf4bba..d6b5bbaf 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -211,11 +211,38 @@ function attachNipMethodAliases(signer) { } } +function hydrateExtensionSignerCapabilities(signer) { + if (!signer || typeof signer !== "object" || signer.type !== "extension") { + return; + } + + const extension = + typeof window !== "undefined" && window && window.nostr ? window.nostr : null; + if (!extension) { + return; + } + + if (typeof signer.signEvent !== "function" && extension.signEvent) { + if (typeof extension.signEvent === "function") { + signer.signEvent = extension.signEvent.bind(extension); + } + } + + if (!signer.nip04 && extension.nip04) { + signer.nip04 = extension.nip04; + } + + if (!signer.nip44 && extension.nip44) { + signer.nip44 = extension.nip44; + } +} + function setActiveSigner(signer) { if (!signer || typeof signer !== "object") { return; } + hydrateExtensionSignerCapabilities(signer); attachNipMethodAliases(signer); activeSigner = signer; @@ -242,6 +269,7 @@ function resolveActiveSigner(pubkey) { const normalized = pubkey.trim().toLowerCase(); const direct = activeSignerRegistry.get(normalized); if (direct) { + hydrateExtensionSignerCapabilities(direct); return direct; } if ( @@ -249,9 +277,11 @@ function resolveActiveSigner(pubkey) { typeof activeSigner.pubkey === "string" && activeSigner.pubkey.trim().toLowerCase() === normalized ) { + hydrateExtensionSignerCapabilities(activeSigner); return activeSigner; } } + hydrateExtensionSignerCapabilities(activeSigner); return activeSigner; } diff --git a/tests/nostr/signers.test.mjs b/tests/nostr/signers.test.mjs new file mode 100644 index 00000000..a4cb2591 --- /dev/null +++ b/tests/nostr/signers.test.mjs @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + setActiveSigner, + resolveActiveSigner, + clearActiveSigner, +} = await import("../../js/nostr.js"); + +test("setActiveSigner hydrates extension capabilities from window.nostr", async () => { + clearActiveSigner(); + const originalWindow = globalThis.window; + const signEvent = async (event) => ({ ...event, id: "signed", sig: "sig" }); + const extension = { + signEvent, + nip04: { encrypt: () => "cipher", decrypt: () => "plain" }, + nip44: { encrypt: () => "cipher44", decrypt: () => "plain44" }, + }; + globalThis.window = { nostr: extension }; + + try { + setActiveSigner({ type: "extension", pubkey: "deadbeef" }); + const signer = resolveActiveSigner("deadbeef"); + assert.ok(signer, "signer should be returned"); + assert.equal(typeof signer.signEvent, "function", "signEvent should be hydrated"); + const result = await signer.signEvent({ pubkey: "deadbeef", kind: 1, content: "" }); + assert.equal(result.id, "signed", "hydrated signEvent should be used"); + assert.equal(typeof signer.nip04Encrypt, "function", "nip04 alias should be attached"); + assert.equal(typeof signer.nip44Encrypt, "function", "nip44 alias should be attached"); + } finally { + clearActiveSigner(); + if (originalWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } + } +});