mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[dm] Fix NIP-17 send failing with nip17-keygen-failed (missing bytesToHex import)
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]>
This commit is contained in:
1 parent
a4549f967f
commit
a1ed04b22c
3 files changed
+162
-1
No files matched your search
+1
-1
@@ -173,7 +173,7 @@ import {
|
||||
readStoredSessionActorEntry,
|
||||
isSessionActor,
|
||||
} from "./sessionActor.js";
|
||||
import { HEX64_REGEX, normalizeHexHash } from "../utils/hex.js";
|
||||
import { HEX64_REGEX, normalizeHexHash, bytesToHex } from "../utils/hex.js";
|
||||
import {
|
||||
NIP46_RPC_KIND,
|
||||
NIP46_HANDSHAKE_TIMEOUT_MS,
|
||||
|
||||
@@ -12,6 +12,16 @@ export function normalizeHexString(value) {
|
||||
export const normalizeHexId = normalizeHexString;
|
||||
export const normalizeHexPubkey = normalizeHexString;
|
||||
|
||||
// Lowercase hex encoding of a byte array (e.g. a generated secret key).
|
||||
export function bytesToHex(bytes) {
|
||||
if (!bytes) {
|
||||
return "";
|
||||
}
|
||||
return Array.from(bytes)
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function normalizeHexHash(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user