mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Sending a NIP-17 DM builds a kind-1059 gift wrap, which needs a fresh ephemeral keypair. generateEphemeralKeypair() hex-encodes the generated secret with bytesToHex — but bytesToHex was never imported in client.js, so the call threw ReferenceError. The try/catch swallowed it into privateKey="", keygen returned null, and every NIP-17 send failed with "We couldn't create secure wrapper keys for NIP-17 delivery." (nip17-keygen-failed). The seal (kind 13) signed fine because it uses the active signer; only the ephemeral keygen was broken. Build and lint don't parse JS, so the missing import wasn't caught. - Add bytesToHex to the canonical js/utils/hex.js and import it in client.js. - Add tests/nip17-send-gift-wrap.test.mjs: forces the NIP-17 path (useNip17 + relay hints, nip44-capable signer) and asserts a kind-1059 gift wrap is published. Reverting the import reproduces the exact nip17-keygen-failed error; the existing DM tests only covered the legacy kind-4/NIP-04 path (they delete nip44Encrypt), so this path had no coverage. Co-Authored-By: Claude Opus 4.8 <[email protected]>
152 lines
5.3 KiB
JavaScript
152 lines
5.3 KiB
JavaScript
// Sending a NIP-17 DM (kind 1059 gift wrap) must generate an ephemeral wrapper
|
|
// keypair. That keygen hex-encodes the generated secret via bytesToHex; a
|
|
// missing import made bytesToHex throw (swallowed into "") so keygen returned
|
|
// null and every NIP-17 send failed with `nip17-keygen-failed`. The legacy
|
|
// (kind 4 / NIP-04) send tests delete nip44Encrypt, so they never exercised
|
|
// this path. This test forces the NIP-17 path and asserts a gift wrap is built
|
|
// and published.
|
|
//
|
|
// test_integrity_note:
|
|
// change_type: ["new_tests"]
|
|
// scenarios:
|
|
// - id: SCN-nip17-send-gift-wrap
|
|
// given: "a nip44-capable signer and known DM relays"
|
|
// when: "sendDirectMessage is called with useNip17:true"
|
|
// then: "it resolves ok and publishes a kind-1059 gift wrap (no keygen failure)"
|
|
// observable_outcomes:
|
|
// - "result.ok === true (not { error: 'nip17-keygen-failed' })"
|
|
// - "a kind 1059 gift-wrap event is published to the recipient relay"
|
|
// determinism_controls:
|
|
// - "real nostr-tools injected; pool.publish/list stubbed; relay hints supplied"
|
|
// anti_cheat_rationale:
|
|
// prevents: ["hard-coded return value", "over-mocking internal logic"]
|
|
// relaxation:
|
|
// did_relax_any_assertion: false
|
|
|
|
import assert from "node:assert/strict";
|
|
import test, { after } from "node:test";
|
|
|
|
const RELAY_URL = "wss://relay.nip17.test";
|
|
|
|
after(() => {
|
|
setTimeout(() => process.exit(0), 100);
|
|
});
|
|
|
|
test("sendDirectMessage (NIP-17) builds and publishes a kind-1059 gift wrap", async () => {
|
|
const previousCanonical = globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
|
const previousNostrTools = globalThis.NostrTools;
|
|
const previousReady = globalThis.nostrToolsReady;
|
|
|
|
const nostrTools = await import("nostr-tools");
|
|
const canonicalTools = { ...nostrTools };
|
|
|
|
try {
|
|
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools;
|
|
globalThis.NostrTools = canonicalTools;
|
|
globalThis.nostrToolsReady = Promise.resolve({ ok: true, value: canonicalTools });
|
|
|
|
const [{ nostrClient }, { getActiveSigner, clearActiveSigner }] =
|
|
await Promise.all([
|
|
import("../js/nostrClientFacade.js"),
|
|
import("../js/nostr/client.js"),
|
|
]);
|
|
|
|
const previousPool = nostrClient.pool;
|
|
const previousRelays = nostrClient.relays;
|
|
const previousPubkey = nostrClient.pubkey;
|
|
|
|
const publishInvocations = [];
|
|
nostrClient.pool = {
|
|
publish(urls, event) {
|
|
publishInvocations.push({ urls, event });
|
|
return {
|
|
on(eventName, handler) {
|
|
if (eventName === "ok") setTimeout(() => handler(), 0);
|
|
return true;
|
|
},
|
|
};
|
|
},
|
|
list() {
|
|
return Promise.resolve([]);
|
|
},
|
|
};
|
|
nostrClient.relays = [RELAY_URL];
|
|
nostrClient.readRelays = [RELAY_URL];
|
|
nostrClient.writeRelays = [RELAY_URL];
|
|
|
|
const senderSecret = canonicalTools.generateSecretKey();
|
|
const senderPubkey = canonicalTools.getPublicKey(senderSecret);
|
|
const recipientSecret = canonicalTools.generateSecretKey();
|
|
const recipientPubkey = canonicalTools.getPublicKey(recipientSecret);
|
|
|
|
await nostrClient.registerPrivateKeySigner({
|
|
privateKey: canonicalTools.utils.bytesToHex(senderSecret),
|
|
pubkey: senderPubkey,
|
|
});
|
|
nostrClient.pubkey = senderPubkey;
|
|
|
|
const signer = getActiveSigner();
|
|
// Keep nip44 (so the NIP-17 path is taken) and give it a working impl.
|
|
signer.signEvent = (event) => {
|
|
const finalized = canonicalTools.finalizeEvent(event, senderSecret);
|
|
return { ...event, id: finalized.id, sig: finalized.sig };
|
|
};
|
|
signer.nip44Encrypt = async (peerPubkey, plaintext) => {
|
|
const key = canonicalTools.nip44.v2.utils.getConversationKey(
|
|
senderSecret,
|
|
peerPubkey,
|
|
);
|
|
return canonicalTools.nip44.v2.encrypt(plaintext, key);
|
|
};
|
|
|
|
const result = await nostrClient.sendDirectMessage(
|
|
recipientPubkey,
|
|
"nip17 gift wrap smoke test",
|
|
{
|
|
useNip17: true,
|
|
recipientRelayHints: [RELAY_URL],
|
|
senderRelayHints: [RELAY_URL],
|
|
},
|
|
);
|
|
|
|
assert.equal(
|
|
result.ok,
|
|
true,
|
|
`NIP-17 send should succeed, got: ${JSON.stringify(result)}`,
|
|
);
|
|
assert.notEqual(
|
|
result.error,
|
|
"nip17-keygen-failed",
|
|
"ephemeral wrapper keygen must not fail",
|
|
);
|
|
|
|
const giftWraps = publishInvocations.filter(
|
|
({ event }) => event?.kind === 1059,
|
|
);
|
|
assert.ok(
|
|
giftWraps.length >= 1,
|
|
"at least one kind-1059 gift wrap should be published",
|
|
);
|
|
// A gift wrap is signed by a fresh ephemeral key, never the sender's.
|
|
assert.notEqual(
|
|
giftWraps[0].event.pubkey,
|
|
senderPubkey,
|
|
"gift wrap must be authored by the ephemeral wrapper key, not the sender",
|
|
);
|
|
|
|
signer.signEvent = undefined;
|
|
signer.nip44Encrypt = undefined;
|
|
clearActiveSigner();
|
|
nostrClient.pool = previousPool;
|
|
nostrClient.relays = previousRelays;
|
|
nostrClient.pubkey = previousPubkey;
|
|
} finally {
|
|
if (previousCanonical === undefined) delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
|
else globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonical;
|
|
if (previousNostrTools === undefined) delete globalThis.NostrTools;
|
|
else globalThis.NostrTools = previousNostrTools;
|
|
if (previousReady === undefined) delete globalThis.nostrToolsReady;
|
|
else globalThis.nostrToolsReady = previousReady;
|
|
}
|
|
});
|