From b0d0dbdba732c7f1de9458d4ba2f23cd3e342f6f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:21:08 -0500 Subject: [PATCH] Add signer capabilities for DM encryption --- js/nostr/adapters/nip07Adapter.js | 15 ++++++- js/nostr/adapters/nip46Adapter.js | 24 +++++++++- js/nostr/adapters/nsecAdapter.js | 12 +++++ js/nostr/client.js | 74 ++++++++++++++++++++++++++++--- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/js/nostr/adapters/nip07Adapter.js b/js/nostr/adapters/nip07Adapter.js index e9dd84c2..ba7453c1 100644 --- a/js/nostr/adapters/nip07Adapter.js +++ b/js/nostr/adapters/nip07Adapter.js @@ -27,9 +27,17 @@ export async function createNip07Adapter(extension) { signEvent: async () => { throw new Error("NIP-07 extension unavailable."); }, - requestPermissions: async () => ({ ok: false, error: new Error("extension-unavailable") }), + requestPermissions: async () => ({ + ok: false, + error: new Error("extension-unavailable"), + }), destroy: async () => {}, canSign: () => false, + capabilities: { + sign: false, + nip44: false, + nip04: false, + }, }; } @@ -92,6 +100,11 @@ export async function createNip07Adapter(extension) { requestPermissions, destroy: async () => {}, canSign: () => typeof resolvedExtension.signEvent === "function", + capabilities: { + sign: typeof resolvedExtension.signEvent === "function", + nip44: Boolean(nip44Encrypt || nip44Decrypt), + nip04: Boolean(nip04Encrypt || nip04Decrypt), + }, }; if (nip04Encrypt) { diff --git a/js/nostr/adapters/nip46Adapter.js b/js/nostr/adapters/nip46Adapter.js index 0541ab6a..7813e5f6 100644 --- a/js/nostr/adapters/nip46Adapter.js +++ b/js/nostr/adapters/nip46Adapter.js @@ -21,9 +21,17 @@ export async function createNip46Adapter(client) { signEvent: async () => { throw new Error("NIP-46 client unavailable."); }, - requestPermissions: async () => ({ ok: false, error: new Error("client-unavailable") }), + requestPermissions: async () => ({ + ok: false, + error: new Error("client-unavailable"), + }), destroy: async () => {}, canSign: () => false, + capabilities: { + sign: false, + nip44: false, + nip04: false, + }, }; } @@ -47,6 +55,13 @@ export async function createNip46Adapter(client) { }, canSign: () => !resolvedClient.destroyed && typeof resolvedClient.signEvent === "function", + capabilities: { + sign: + !resolvedClient.destroyed && + typeof resolvedClient.signEvent === "function", + nip44: false, + nip04: false, + }, }; if (typeof resolvedClient.nip04Encrypt === "function") { @@ -62,5 +77,12 @@ export async function createNip46Adapter(client) { signer.nip44Decrypt = resolvedClient.nip44Decrypt.bind(resolvedClient); } + signer.capabilities.nip04 = Boolean( + signer.nip04Encrypt || signer.nip04Decrypt + ); + signer.capabilities.nip44 = Boolean( + signer.nip44Encrypt || signer.nip44Decrypt + ); + return signer; } diff --git a/js/nostr/adapters/nsecAdapter.js b/js/nostr/adapters/nsecAdapter.js index 5ce8ef6a..5bb94cc2 100644 --- a/js/nostr/adapters/nsecAdapter.js +++ b/js/nostr/adapters/nsecAdapter.js @@ -59,6 +59,11 @@ export async function createNsecAdapter({ privateKey, pubkey } = {}) { requestPermissions: async () => ({ ok: true }), destroy: async () => {}, canSign: () => Boolean(normalizedPrivateKey), + capabilities: { + sign: Boolean(normalizedPrivateKey), + nip44: false, + nip04: false, + }, }; if (typeof cipherClosures.nip04Encrypt === "function") { @@ -74,5 +79,12 @@ export async function createNsecAdapter({ privateKey, pubkey } = {}) { signer.nip44Decrypt = cipherClosures.nip44Decrypt; } + signer.capabilities.nip04 = Boolean( + signer.nip04Encrypt || signer.nip04Decrypt + ); + signer.capabilities.nip44 = Boolean( + signer.nip44Encrypt || signer.nip44Decrypt + ); + return signer; } diff --git a/js/nostr/client.js b/js/nostr/client.js index 9f235ca0..b9f9c160 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -255,6 +255,40 @@ function attachNipMethodAliases(signer) { } } +function resolveSignerCapabilities(signer) { + const fallback = { + sign: false, + nip44: false, + nip04: false, + }; + + if (!signer || typeof signer !== "object") { + return fallback; + } + + const capabilities = + signer.capabilities && typeof signer.capabilities === "object" + ? signer.capabilities + : {}; + + return { + sign: + typeof capabilities.sign === "boolean" + ? capabilities.sign + : typeof signer.signEvent === "function", + nip44: + typeof capabilities.nip44 === "boolean" + ? capabilities.nip44 + : typeof signer.nip44Encrypt === "function" || + typeof signer.nip44Decrypt === "function", + nip04: + typeof capabilities.nip04 === "boolean" + ? capabilities.nip04 + : typeof signer.nip04Encrypt === "function" || + typeof signer.nip04Decrypt === "function", + }; +} + function hydrateExtensionSignerCapabilities(signer) { if (!signer || typeof signer !== "object" || signer.type !== "extension") { return; @@ -288,6 +322,7 @@ function setActiveSigner(signer) { hydrateExtensionSignerCapabilities(signer); attachNipMethodAliases(signer); + signer.capabilities = resolveSignerCapabilities(signer); const pubkey = typeof signer.pubkey === "string" && signer.pubkey.trim() @@ -3966,7 +4001,11 @@ export class NostrClient { } if (activeSigner) { - if (typeof activeSigner.nip44Decrypt === "function") { + const capabilities = resolveSignerCapabilities(activeSigner); + if ( + capabilities.nip44 && + typeof activeSigner.nip44Decrypt === "function" + ) { addCandidate( "nip44", activeSigner.nip44Decrypt.bind(activeSigner), @@ -3977,7 +4016,10 @@ export class NostrClient { }, ); } - if (typeof activeSigner.nip04Decrypt === "function") { + if ( + capabilities.nip04 && + typeof activeSigner.nip04Decrypt === "function" + ) { addCandidate( "nip04", activeSigner.nip04Decrypt.bind(activeSigner), @@ -4337,13 +4379,21 @@ export class NostrClient { } const encryptionCandidates = []; - if (typeof signer.nip44Encrypt === "function") { + const signerCapabilities = resolveSignerCapabilities(signer); + const allowNip04Fallback = signerCapabilities.nip04 === true; + if ( + signerCapabilities.nip44 && + typeof signer.nip44Encrypt === "function" + ) { encryptionCandidates.push({ scheme: "nip44", encrypt: signer.nip44Encrypt, }); } - if (typeof signer.nip04Encrypt === "function") { + if ( + signerCapabilities.nip04 && + typeof signer.nip04Encrypt === "function" + ) { encryptionCandidates.push({ scheme: "nip04", encrypt: signer.nip04Encrypt, @@ -4375,7 +4425,21 @@ export class NostrClient { typeof sessionActor.privateKey === "string" && sessionActor.privateKey; - if (sessionMatchesActor) { + const canFallbackWithSession = sessionMatchesActor && allowNip04Fallback; + + if (!encryptionCandidates.length && !canFallbackWithSession) { + const error = new Error( + "Your signer does not support NIP-44 or NIP-04 encryption.", + ); + userLogger.warn("[nostr] Encryption unsupported for DM send.", error); + return { + ok: false, + error: "encryption-unsupported", + details: error, + }; + } + + if (canFallbackWithSession) { try { const tools = (await ensureNostrTools()) || getCachedNostrTools(); if (tools?.nip04 && typeof tools.nip04.encrypt === "function") {