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}`, + ); +});