From 7951fd0f098a722e371bd4e860a2bc77a68f65c6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:52:11 -0400 Subject: [PATCH] fix(dm): can't open conversations + "helpers unavailable" on a working signer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After switching to a responsive signer (nos2x), two DM-specific bugs surfaced. 1. Clicking a conversation in the list threw "this.controller.handleDmConversationSelect is not a function" (ProfileDirectMessageRenderer onSelectConversation). The method exists on ProfileDirectMessageActions and the controller delegates ~15 sibling Actions methods — but the handleDmConversationSelect delegation was missing. Add it. 2. Loading a conversation's messages threw "DM decryption helpers are unavailable" even though the signer decrypts blocks/subscriptions/hashtags fine. buildDmDecryptContext gated decryptor registration on (a) the dynamic `capabilities` getter (which probes window.nostr and false-negatives on lazy injection / nos2x's module shape) and (b) the permission-probe result. The list services already learned to register by METHOD EXISTENCE and validate at call time; align the DM path to do the same so a present, working signer never yields zero decryptors. The permission probe still runs. Tests: tests/dm-decrypt-context.test.mjs (cheat-resistant — a signer whose capabilities getter reports false and whose permission probe returns ok:false must still register nip04+nip44 decryptors). Full unit + dm:unit + dm:integration suites green. KNOWN_BUGS #0. Co-Authored-By: Claude Opus 4.8 --- js/nostr/client.js | 22 ++++--- .../ProfileDirectMessageController.js | 4 ++ tests/dm-decrypt-context.test.mjs | 62 +++++++++++++++++++ 3 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 tests/dm-decrypt-context.test.mjs diff --git a/js/nostr/client.js b/js/nostr/client.js index 3cdb40b3..e0f04168 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -1264,12 +1264,17 @@ export class NostrClient { } } - if (activeSigner && extensionPermissionResult?.ok !== false) { - const capabilities = resolveSignerCapabilities(activeSigner); - if ( - capabilities.nip44 && - typeof activeSigner.nip44Decrypt === "function" - ) { + // Register the active signer's decryptors by METHOD EXISTENCE, not by the + // dynamic capabilities getter or the permission-request result. The getter + // probes window.nostr for injected nip04/nip44 modules and gives false + // negatives (lazy injection, or providers like nos2x that expose decrypt + // methods which validate at call time) — the list services already learned + // this. Hard-gating here returned ZERO decryptors and made the DM load throw + // "DM decryption helpers are unavailable" even though the signer decrypts + // everything else fine (KNOWN_BUGS #0). The permission request above still + // runs (it can grant), it just no longer blocks decryptor registration. + if (activeSigner) { + if (typeof activeSigner.nip44Decrypt === "function") { addCandidate( "nip44", (pubkey, ciphertext, options) => @@ -1289,10 +1294,7 @@ export class NostrClient { }, ); } - if ( - capabilities.nip04 && - typeof activeSigner.nip04Decrypt === "function" - ) { + if (typeof activeSigner.nip04Decrypt === "function") { addCandidate( "nip04", (pubkey, ciphertext, options) => diff --git a/js/ui/profileModal/ProfileDirectMessageController.js b/js/ui/profileModal/ProfileDirectMessageController.js index c9ec07a4..41180786 100644 --- a/js/ui/profileModal/ProfileDirectMessageController.js +++ b/js/ui/profileModal/ProfileDirectMessageController.js @@ -599,6 +599,10 @@ export class ProfileDirectMessageController { return this.actions.pauseProfileMessages(...args); } + handleDmConversationSelect(...args) { + return this.actions.handleDmConversationSelect(...args); + } + handleSendDmRequest(...args) { return this.actions.handleSendDmRequest(...args); } diff --git a/tests/dm-decrypt-context.test.mjs b/tests/dm-decrypt-context.test.mjs new file mode 100644 index 00000000..3c4bd4ae --- /dev/null +++ b/tests/dm-decrypt-context.test.mjs @@ -0,0 +1,62 @@ +// Scenario (SCN-dm-context-method-existence): +// Given an active NIP-07 signer that EXPOSES nip04Decrypt/nip44Decrypt methods +// but whose dynamic `capabilities` getter reports them false (the real-env +// nos2x case — lazy/odd module shape) AND whose extension permission probe +// returns ok:false, +// When buildDmDecryptContext runs for a DM load, +// Then it must still register the signer's decryptors (by method existence, +// like the list services do) — otherwise listDirectMessages throws +// "DM decryption helpers are unavailable" even though the signer decrypts +// everything else fine (KNOWN_BUGS #0). + +import test from "node:test"; +import assert from "node:assert/strict"; + +if (!globalThis.WebSocket) { + globalThis.WebSocket = class MockWebSocket {}; +} + +const { NostrClient } = await import("../js/nostr/client.js"); + +test("buildDmDecryptContext registers decryptors by method existence, not capabilities/permission gate", async () => { + const client = new NostrClient(); + const actor = "a".repeat(64); + client.pubkey = actor; + + const fakeSigner = { + type: "nip07", + pubkey: actor, + nip04Decrypt: async () => "plaintext", + nip44Decrypt: async () => "plaintext", + // The dynamic capabilities getter that returned false negatives and made + // the DM path register zero decryptors. + get capabilities() { + return { sign: true, nip04: false, nip44: false }; + }, + }; + + // Active signer resolves to our fake regardless of how it's looked up. + client.signerManager.resolveActiveSigner = () => fakeSigner; + client.signerManager.getActiveSigner = () => fakeSigner; + // Permission probe reports NOT ok — must NOT block decryptor registration. + client.ensureExtensionPermissions = async () => ({ + ok: false, + error: new Error("permission-not-granted"), + }); + + const context = await client.buildDmDecryptContext(actor); + + const schemes = context.decryptors.map((d) => d.scheme); + assert.ok( + schemes.includes("nip44"), + "nip44 decryptor must be registered from the signer method", + ); + assert.ok( + schemes.includes("nip04"), + "nip04 decryptor must be registered from the signer method", + ); + assert.ok( + context.decryptors.length >= 2, + `expected at least the two signer decryptors, got ${context.decryptors.length}`, + ); +});