diff --git a/js/nostr/client.js b/js/nostr/client.js index a56f7600..8f81c3c8 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -4229,8 +4229,9 @@ export class NostrClient { } /** - * Edits a video by creating a *new event* with a brand-new d tag, - * but reuses the same videoRootId as the original. + * Edits a video by creating a new event that reuses the existing d tag + * so subsequent publishes overwrite the same NIP-33 addressable record + * while keeping the original videoRootId. * * This version forces version=2 for the original note and uses * lowercase comparison for public keys. @@ -4322,8 +4323,16 @@ export class NostrClient { const oldRootId = baseEvent.videoRootId || baseEvent.id; const preservedDTag = this.resolveEventDTag(baseEvent, originalEventStub); + const fallbackDTag = + (typeof baseEvent.id === "string" && baseEvent.id.trim()) || + (typeof originalEventStub?.id === "string" && originalEventStub.id.trim()) || + ""; const finalDTagValue = - preservedDTag || `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`; + (typeof preservedDTag === "string" && preservedDTag.trim()) || fallbackDTag; + + if (!finalDTagValue) { + throw new Error("Unable to determine a stable d tag for this edit."); + } // Build the updated content object const contentObject = { diff --git a/tests/nostr/edit-video-dtag.test.mjs b/tests/nostr/edit-video-dtag.test.mjs index 34d73f1e..dd3b5070 100644 --- a/tests/nostr/edit-video-dtag.test.mjs +++ b/tests/nostr/edit-video-dtag.test.mjs @@ -96,3 +96,71 @@ test("editVideo preserves existing d tag", async (t) => { "editVideo should preserve the original videoRootId", ); }); + +test("editVideo falls back to base event id when d tag missing", async (t) => { + const client = new NostrClient(); + const pubkey = "ABC123"; + const baseEvent = { + id: "event-original-no-d", + pubkey, + version: 3, + videoRootId: "root-abc", + title: "Original Title", + url: "https://example.com/video.mp4", + magnet: "magnet:?xt=urn:btih:example", + ws: "wss://example.com", + xs: "https://example.com/file.torrent", + enableComments: true, + isPrivate: false, + isNsfw: false, + isForKids: false, + nip71: null, + thumbnail: "https://example.com/thumb.jpg", + description: "original", + mode: "live", + tags: [["t", "video"]], + }; + + client.getEventById = async (id) => (id === baseEvent.id ? baseEvent : null); + client.relays = ["wss://relay.one"]; + client.pool = createStubPool(); + client.ensureExtensionPermissions = async () => ({ ok: true }); + + const signedEvents = []; + const signer = { + type: "app", + pubkey, + signEvent: async (event) => { + signedEvents.push(event); + return { ...event, id: `signed-${signedEvents.length}` }; + }, + }; + + setActiveSigner(signer); + client.ensureActiveSignerForPubkey = async () => signer; + + t.after(() => { + clearActiveSigner(); + signedEvents.length = 0; + }); + + const result = await client.editVideo({ id: baseEvent.id }, { title: "Updated" }, pubkey); + + assert.ok(result, "editVideo should return the signed event"); + assert.equal(signedEvents.length, 1, "editVideo should sign exactly one event"); + const signedEvent = signedEvents[0]; + assert.ok(signedEvent, "signed event should be captured"); + const dTags = signedEvent.tags.filter((tag) => Array.isArray(tag) && tag[0] === "d"); + assert.deepEqual( + dTags, + [["d", baseEvent.id]], + "editVideo should fall back to the base event id for the d tag when none exists", + ); + + const parsedContent = JSON.parse(signedEvent.content); + assert.equal( + parsedContent.videoRootId, + baseEvent.videoRootId, + "editVideo should continue to use the original videoRootId", + ); +});