From ac1dcff184905e11bc49ae1bd581046bc59daf88 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:28:27 -0400 Subject: [PATCH] fix(publish): flag + log unconfirmed (optimistic) relay publishes publishEventToRelay optimistically reports success when a relay handle exposes no ok/failed event and isn't thenable (legacy 'seen'-only relays). That made an UNCONFIRMED publish indistinguishable from a real ack, so a note could show 'Published' with nothing actually stored. The result now carries optimistic:true for that path (callers can distinguish it from a confirmed ack) and logs a dev.warn. Behavior is otherwise unchanged (still treated as success for legacy relays). Publish audit #3. Test: tests/nostr-publish-optimistic.test.mjs. Co-Authored-By: Claude Opus 4.8 --- js/nostrPublish.js | 14 +++++-- tests/nostr-publish-optimistic.test.mjs | 49 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/nostr-publish-optimistic.test.mjs diff --git a/js/nostrPublish.js b/js/nostrPublish.js index d389bf67..bc33fc12 100644 --- a/js/nostrPublish.js +++ b/js/nostrPublish.js @@ -68,12 +68,12 @@ export function publishEventToRelay(pool, url, event, options = {}) { return new Promise((resolve) => { let settled = false; - const finalize = (success, error = null) => { + const finalize = (success, error = null, optimistic = false) => { if (settled) { return; } settled = true; - resolve({ url, success, error }); + resolve({ url, success, error, optimistic }); }; const timeoutId = setTimeout(() => { @@ -149,8 +149,16 @@ export function publishEventToRelay(pool, url, event, options = {}) { return; } + // No confirmation channel: the relay handle exposed neither an ok/failed + // event nor a thenable. We still optimistically report success for legacy + // "seen"-only relays, but mark the result `optimistic` and log it so a + // silently-unconfirmed publish is traceable rather than indistinguishable + // from a real relay ack (publish audit #3). clearTimeout(timeoutId); - finalize(true); + devLogger.warn( + `[nostrPublish] Optimistic (unconfirmed) publish success for ${url}: relay gave no ok/failed/then signal.`, + ); + finalize(true, null, true); }); } diff --git a/tests/nostr-publish-optimistic.test.mjs b/tests/nostr-publish-optimistic.test.mjs new file mode 100644 index 00000000..0438b1a5 --- /dev/null +++ b/tests/nostr-publish-optimistic.test.mjs @@ -0,0 +1,49 @@ +// Scenario (SCN-publish-optimistic-flag): +// publishEventToRelay optimistically reports success when a relay handle gives +// no ok/failed/then signal (needed for legacy "seen"-only relays). Previously +// such an UNCONFIRMED success was indistinguishable from a real relay ack, so +// a note could show "Published" with nothing actually stored (audit #3). +// Given a relay handle with no confirmation channel, +// Then the result is success:true but flagged optimistic:true; +// Given a relay that emits 'ok', +// Then the result is success:true and optimistic:false (a real ack). + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { publishEventToRelay } from "../js/nostrPublish.js"; + +const EVENT = { id: "evt", kind: 1, content: "x" }; + +test("unconfirmed publish (no ok/failed/then) is flagged optimistic", async () => { + const pool = { publish: () => undefined }; // no handle at all + const result = await publishEventToRelay(pool, "wss://relay.example", EVENT, { + timeoutMs: 1000, + }); + assert.equal(result.success, true, "still reports success for legacy relays"); + assert.equal(result.optimistic, true, "but marks it as unconfirmed"); +}); + +test("a real 'ok' ack is NOT optimistic", async () => { + const pool = { + publish: () => ({ + on(ev, handler) { + if (ev === "ok") setTimeout(() => handler(), 0); + }, + }), + }; + const result = await publishEventToRelay(pool, "wss://relay.example", EVENT, { + timeoutMs: 1000, + }); + assert.equal(result.success, true); + assert.equal(result.optimistic, false, "confirmed ack must not be optimistic"); +}); + +test("a thenable publish that resolves is a confirmed (non-optimistic) success", async () => { + const pool = { publish: () => Promise.resolve() }; + const result = await publishEventToRelay(pool, "wss://relay.example", EVENT, { + timeoutMs: 1000, + }); + assert.equal(result.success, true); + assert.equal(result.optimistic, false); +});