diff --git a/js/nostr/client.js b/js/nostr/client.js index d6b5bbaf..cf7e6b19 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -3378,6 +3378,15 @@ export class NostrClient { async publishVideo(videoPayload, pubkey) { if (!pubkey) throw new Error("Not logged in to publish video."); + const normalizedPubkey = + typeof pubkey === "string" ? pubkey.trim() : ""; + + if (!normalizedPubkey) { + throw new Error("Not logged in to publish video."); + } + + const userPubkeyLower = normalizedPubkey.toLowerCase(); + const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload); const nip71EditedFlag = videoPayload && typeof videoPayload === "object" @@ -3389,7 +3398,7 @@ export class NostrClient { // controllers and revert helpers so all paths stay in lockstep. devLogger.log("Publishing new video with data:", videoData); if (nip71Metadata) { - devLogger.log("Including NIP-71 metadata:", nip71Metadata); + devLogger.log("Including NIP-71 metadata:", nip71Metadata); } const rawMagnet = typeof videoData.magnet === "string" ? videoData.magnet : ""; @@ -3455,7 +3464,7 @@ export class NostrClient { ); const event = buildVideoPostEvent({ - pubkey, + pubkey: normalizedPubkey, created_at: createdAt, dTagValue, content: contentObject, @@ -3463,7 +3472,7 @@ export class NostrClient { }); devLogger.log("Publish event with brand-new root:", videoRootId); - devLogger.log("Event content:", event.content); + devLogger.log("Event content:", event.content); try { const { signedEvent } = await this.signAndPublishEvent(event, { @@ -3496,7 +3505,7 @@ export class NostrClient { } const mirrorEvent = buildVideoMirrorEvent({ - pubkey, + pubkey: normalizedPubkey, created_at: createdAt, tags: mirrorTags, content: altText, @@ -3536,10 +3545,10 @@ export class NostrClient { description: contentObject.description, url: contentObject.url, magnet: contentObject.magnet, - thumbnail: contentObject.thumbnail, - mode: contentObject.mode, - isPrivate: wantPrivate, - isNsfw: contentObject.isNsfw, + thumbnail: contentObject.thumbnail, + mode: contentObject.mode, + isPrivate: wantPrivate, + isNsfw: contentObject.isNsfw, isForKids: contentObject.isForKids, }; @@ -3559,8 +3568,8 @@ export class NostrClient { }, userPubkeyLower, { - videoRootId: oldRootId, - dTag: newD, + videoRootId, + dTag: dTagValue, eventId: signedEvent.id, } ); diff --git a/tests/nostr-publish-rejection.test.mjs b/tests/nostr-publish-rejection.test.mjs index e97d8556..28640cda 100644 --- a/tests/nostr-publish-rejection.test.mjs +++ b/tests/nostr-publish-rejection.test.mjs @@ -283,9 +283,85 @@ try { } } + async function testPublishVideoNormalizesPubkeyForNip71Metadata() { + const uppercasePubkey = "AB".repeat(32); + const originalSignAndPublish = nostrClient.signAndPublishEvent; + const originalPublishNip71 = nostrClient.publishNip71Video; + + const publishedEvents = []; + const nip71Calls = []; + + nostrClient.signAndPublishEvent = async (event, options = {}) => { + publishedEvents.push({ event, options }); + const contextLabel = + typeof options?.context === "string" && options.context + ? options.context + : "event"; + return { + signedEvent: { + ...event, + id: event?.id || `signed-${contextLabel}`, + }, + }; + }; + + nostrClient.publishNip71Video = async (payload, pubkey, pointerOptions) => { + nip71Calls.push({ payload, pubkey, pointerOptions }); + return null; + }; + + try { + const result = await nostrClient.publishVideo( + { + legacyFormData: { + title: "Metadata Video", + description: "Video with metadata", + url: "https://cdn.example/video.mp4", + mode: "live", + }, + nip71: { + title: "Metadata Video", + imeta: [{ url: "https://cdn.example/video.mp4", m: "video/mp4" }], + }, + }, + uppercasePubkey, + ); + + assert.equal(typeof result?.id, "string", "publishVideo should resolve with a signed event"); + assert.ok( + publishedEvents.some((entry) => entry.options?.context === "video note"), + "publishVideo should sign the primary video note", + ); + assert.equal( + nip71Calls.length, + 1, + "publishVideo should invoke publishNip71Video when metadata is provided", + ); + assert.equal( + nip71Calls[0]?.pubkey, + uppercasePubkey.toLowerCase(), + "publishVideo should normalize the pubkey passed to publishNip71Video", + ); + assert.ok( + typeof nip71Calls[0]?.pointerOptions?.videoRootId === "string" && + nip71Calls[0]?.pointerOptions?.videoRootId.length > 0, + "publishVideo should supply pointer metadata for the new video", + ); + assert.ok( + typeof nip71Calls[0]?.pointerOptions?.dTag === "string" && + nip71Calls[0]?.pointerOptions?.dTag.length > 0, + "publishVideo should provide the new d tag as a pointer", + ); + } finally { + nostrClient.signAndPublishEvent = originalSignAndPublish; + nostrClient.publishNip71Video = originalPublishNip71; + } + } + await testPublishVideoRejectsWhenAllRelaysFail(); await testPublishSubscriptionListRejectsWhenAllRelaysFail(); await testPublishBlockListRejectsWhenAllRelaysFail(); + await testPublishVideoNormalizesPubkeyForNip71Metadata(); console.log("nostr publish rejection tests passed"); } finally {