mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Fix direct message encryption fallback and add regression test
This commit is contained in:
1 parent
2a3fc276c1
commit
d8dcdf2b40
3 files changed
+206
-8
No files matched your search
@@ -47,6 +47,7 @@ Automated regression coverage now exercises these flows on the unified design-sy
|
||||
- Sign in with a new nsec or mnemonic, opt into remembering it, and verify the passphrase prompt blocks submission until both passphrase fields match.
|
||||
- Reload the page, select "Unlock saved key", enter the passphrase, and confirm the account unlocks without re-entering the secret.
|
||||
- Publish a video while logged in with the direct-key provider and confirm the resulting event is signed (relays accept it) and the UI reports success.
|
||||
- While still signed in via the direct-key provider, send a direct message to a second account and confirm it decrypts successfully on the recipient before release.
|
||||
- Log out and ensure the session clears decrypted key material; stored keys should require the passphrase to unlock again.
|
||||
|
||||
Document findings (pass/fail notes plus relevant screenshots or logs) so they can be attached to release or PR notes.
|
||||
+71
-8
@@ -8959,10 +8959,6 @@ export class NostrClient {
|
||||
return { ok: false, error: "sign-event-unavailable" };
|
||||
}
|
||||
|
||||
if (typeof signer.nip04Encrypt !== "function") {
|
||||
return { ok: false, error: "nip04-unavailable" };
|
||||
}
|
||||
|
||||
if (shouldRequestExtensionPermissions(signer)) {
|
||||
const permissionResult = await this.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
@@ -9002,11 +8998,78 @@ export class NostrClient {
|
||||
return { ok: false, error: "invalid-target" };
|
||||
}
|
||||
|
||||
const encryptionCandidates = [];
|
||||
if (typeof signer.nip44Encrypt === "function") {
|
||||
encryptionCandidates.push({
|
||||
scheme: "nip44",
|
||||
encrypt: signer.nip44Encrypt,
|
||||
});
|
||||
}
|
||||
if (typeof signer.nip04Encrypt === "function") {
|
||||
encryptionCandidates.push({
|
||||
scheme: "nip04",
|
||||
encrypt: signer.nip04Encrypt,
|
||||
});
|
||||
}
|
||||
|
||||
const encryptionErrors = [];
|
||||
let ciphertext = "";
|
||||
try {
|
||||
ciphertext = await signer.nip04Encrypt(targetHex, trimmedMessage);
|
||||
} catch (error) {
|
||||
return { ok: false, error: "encryption-failed", details: error };
|
||||
|
||||
for (const candidate of encryptionCandidates) {
|
||||
try {
|
||||
const encrypted = await candidate.encrypt(targetHex, trimmedMessage);
|
||||
if (typeof encrypted === "string" && encrypted) {
|
||||
ciphertext = encrypted;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
encryptionErrors.push({ scheme: candidate.scheme, error });
|
||||
}
|
||||
}
|
||||
|
||||
if (!ciphertext) {
|
||||
const normalizedActorHex = normalizeActorKey(actorHex);
|
||||
const sessionActor = this.sessionActor;
|
||||
const sessionMatchesActor =
|
||||
sessionActor &&
|
||||
typeof sessionActor.pubkey === "string" &&
|
||||
sessionActor.pubkey.toLowerCase() === normalizedActorHex &&
|
||||
typeof sessionActor.privateKey === "string" &&
|
||||
sessionActor.privateKey;
|
||||
|
||||
if (sessionMatchesActor) {
|
||||
try {
|
||||
const tools = (await ensureNostrTools()) || getCachedNostrTools();
|
||||
if (tools?.nip04 && typeof tools.nip04.encrypt === "function") {
|
||||
const encrypted = await tools.nip04.encrypt(
|
||||
sessionActor.privateKey,
|
||||
targetHex,
|
||||
trimmedMessage,
|
||||
);
|
||||
if (typeof encrypted === "string" && encrypted) {
|
||||
ciphertext = encrypted;
|
||||
}
|
||||
} else {
|
||||
encryptionErrors.push({
|
||||
scheme: "nip04",
|
||||
error: new Error("nostr-tools nip04 helpers unavailable"),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
encryptionErrors.push({ scheme: "nip04", error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ciphertext) {
|
||||
const details =
|
||||
encryptionErrors.length === 1
|
||||
? encryptionErrors[0].error
|
||||
: encryptionErrors.map((entry) => ({
|
||||
scheme: entry.scheme,
|
||||
error: entry.error,
|
||||
}));
|
||||
return { ok: false, error: "encryption-failed", details };
|
||||
}
|
||||
|
||||
const event = {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const RELAY_URL = "wss://relay.unit.test";
|
||||
|
||||
test("sendDirectMessage succeeds with private key signer and no extension", 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 };
|
||||
if (typeof canonicalTools.signEvent !== "function") {
|
||||
canonicalTools.signEvent = (event, privateKey) => {
|
||||
const finalized = canonicalTools.finalizeEvent(event, privateKey);
|
||||
return finalized.sig;
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools;
|
||||
globalThis.NostrTools = canonicalTools;
|
||||
globalThis.nostrToolsReady = Promise.resolve({
|
||||
ok: true,
|
||||
value: canonicalTools,
|
||||
});
|
||||
|
||||
const { nostrClient, getActiveSigner, clearActiveSigner } = await import(
|
||||
"../js/nostr.js"
|
||||
);
|
||||
|
||||
const previousPool = nostrClient.pool;
|
||||
const previousRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const previousPubkey = nostrClient.pubkey;
|
||||
const previousSessionActor = nostrClient.sessionActor;
|
||||
const previousLockedSessionActor = nostrClient.lockedSessionActor;
|
||||
|
||||
const publishInvocations = [];
|
||||
nostrClient.pool = {
|
||||
publish(urls, event) {
|
||||
publishInvocations.push({ urls, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
setTimeout(() => handler(), 0);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
nostrClient.relays = [RELAY_URL];
|
||||
|
||||
const senderPrivateKey = canonicalTools.utils.bytesToHex(
|
||||
canonicalTools.generateSecretKey(),
|
||||
);
|
||||
const senderPubkey = canonicalTools.getPublicKey(senderPrivateKey);
|
||||
const recipientSecret = canonicalTools.generateSecretKey();
|
||||
const recipientPrivateKey = canonicalTools.utils.bytesToHex(recipientSecret);
|
||||
const recipientPubkey = canonicalTools.getPublicKey(recipientSecret);
|
||||
|
||||
await nostrClient.registerPrivateKeySigner({
|
||||
privateKey: senderPrivateKey,
|
||||
pubkey: senderPubkey,
|
||||
});
|
||||
nostrClient.pubkey = senderPubkey;
|
||||
|
||||
const signer = getActiveSigner();
|
||||
const originalSignEvent = signer?.signEvent;
|
||||
const originalNip04 = signer?.nip04Encrypt;
|
||||
const originalNip44 = signer?.nip44Encrypt;
|
||||
if (signer) {
|
||||
signer.signEvent = (event) => {
|
||||
const finalized = canonicalTools.finalizeEvent(event, senderPrivateKey);
|
||||
return { ...event, id: finalized.id, sig: finalized.sig };
|
||||
};
|
||||
delete signer.nip04Encrypt;
|
||||
delete signer.nip44Encrypt;
|
||||
}
|
||||
|
||||
const message = "bitvid direct message smoke test";
|
||||
const result = await nostrClient.sendDirectMessage(recipientPubkey, message);
|
||||
|
||||
assert.deepEqual(result, { ok: true }, "sendDirectMessage should resolve successfully");
|
||||
assert.equal(publishInvocations.length, 1, "exactly one relay publish should be attempted");
|
||||
|
||||
const [{ urls, event }] = publishInvocations;
|
||||
assert.deepEqual(urls, [RELAY_URL], "publish should target the configured relay");
|
||||
assert.equal(event.kind, 4, "event kind should be 4 for direct messages");
|
||||
assert.equal(event.pubkey, senderPubkey, "event should be authored by the sender");
|
||||
assert.equal(typeof event.content, "string", "ciphertext should be a string");
|
||||
assert.notEqual(event.content, "", "ciphertext should not be empty");
|
||||
|
||||
const decrypted = await canonicalTools.nip04.decrypt(
|
||||
recipientPrivateKey,
|
||||
senderPubkey,
|
||||
event.content,
|
||||
);
|
||||
assert.equal(decrypted, message, "recipient should decrypt the original message");
|
||||
|
||||
if (signer) {
|
||||
signer.signEvent = originalSignEvent;
|
||||
signer.nip04Encrypt = originalNip04;
|
||||
signer.nip44Encrypt = originalNip44;
|
||||
}
|
||||
|
||||
clearActiveSigner();
|
||||
nostrClient.pool = previousPool;
|
||||
nostrClient.relays = previousRelays;
|
||||
nostrClient.pubkey = previousPubkey;
|
||||
nostrClient.sessionActor = previousSessionActor;
|
||||
nostrClient.lockedSessionActor = previousLockedSessionActor;
|
||||
} 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