Files
bitvid/tests/dm-decryptor-order.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 7f6a0f3adf dm: try NIP-04 before NIP-44 for legacy kind-4 messages
orderDecryptors compared candidate `priority` before the `preferLegacy` flag.
The signer registers its nip44 candidate at an earlier priority (-20) than
nip04 (-10), so priority always decided the order and preferLegacy was dead
code — every kind-4 DM tried nip44 first, which fails on the `?iv=` NIP-04
ciphertext ("invalid base64: Unknown letter ?") before falling back to nip04.
That doubled the serialized extension round-trips per message on cold inbox
load and spammed the console.

Move the legacy preference (and it already had the explicit-hint check) above
the priority comparison so kind-4 messages attempt nip04 first. Gift-wrap
(preferGiftWrap) and same-scheme priority ordering are unchanged.

Regression: tests/dm-decryptor-order.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 21:36:15 -04:00

114 lines
4.0 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { decryptDM } from "../js/dmDecryptor.js";
const SENDER_HEX = "a".repeat(64);
const RECIPIENT_HEX = "b".repeat(64);
test("decryptDM prefers higher priority decryptor even if slower", async () => {
// Mock event
const event = {
kind: 4,
content: "ciphertext",
pubkey: SENDER_HEX,
tags: [["p", RECIPIENT_HEX]]
};
const context = {
actorPubkey: RECIPIENT_HEX,
decryptors: [
{
scheme: "preferred",
priority: 0,
source: "slow-preferred",
decrypt: async (remote, cipher) => {
// Normalize remote key might be done inside decryptDM
if (remote === SENDER_HEX) {
await new Promise(r => setTimeout(r, 100));
return "preferred-plaintext";
}
throw new Error("wrong remote: " + remote);
}
},
{
scheme: "fallback",
priority: 10,
source: "fast-fallback",
decrypt: async (remote, cipher) => {
if (remote === SENDER_HEX) {
// Fast!
return "fallback-plaintext";
}
throw new Error("wrong remote: " + remote);
}
}
]
};
const result = await decryptDM(event, context);
assert.equal(result.ok, true, "Result should be ok. Errors: " + JSON.stringify(result.errors));
// If Promise.any is used, it will likely pick "fallback-plaintext" (fast).
// We WANT "preferred-plaintext".
assert.equal(result.plaintext, "preferred-plaintext",
`Expected preferred-plaintext but got ${result.plaintext}. Current implementation likely uses Promise.any which is sensitive to race conditions.`);
});
// Regression: a kind-4 (legacy/NIP-04) DM must attempt the nip04 decryptor FIRST,
// even though the signer registers its nip44 candidate at a lower (earlier)
// `priority`. Previously the priority comparison ran before the legacy
// preference, so every kind-4 message tried nip44 on the `?iv=` ciphertext first
// (guaranteed "invalid base64" failure on the serialized extension) before
// falling back to nip04 — doubling signer round-trips and spamming the console.
test("kind-4 DM tries nip04 before nip44 despite nip44's earlier priority", async () => {
const event = {
kind: 4,
content: "deadbeef?iv=cafe",
pubkey: SENDER_HEX,
tags: [["p", RECIPIENT_HEX]],
};
const calls = [];
const context = {
actorPubkey: RECIPIENT_HEX,
// Mirror the signer registration: nip44 at priority -20, nip04 at -10.
decryptors: [
{
scheme: "nip44",
priority: -20,
source: "extension",
supportsGiftWrap: true,
decrypt: async () => {
calls.push("nip44");
// nip44 on a NIP-04 ?iv= ciphertext always fails.
throw new Error('invalid base64: Unknown letter: "?"');
},
},
{
scheme: "nip04",
priority: -10,
source: "extension",
decrypt: async (remote) => {
calls.push("nip04");
if (remote === SENDER_HEX) {
return "hello";
}
throw new Error("wrong remote: " + remote);
},
},
],
};
const result = await decryptDM(event, context);
assert.equal(result.ok, true, "decrypt should succeed via nip04");
assert.equal(result.plaintext, "hello");
assert.deepEqual(
calls,
["nip04"],
`nip04 must be tried first and succeed without ever calling nip44; got call order ${JSON.stringify(calls)}`,
);
});