I moved my comment events from kind 1 to NIP-22 kind 1111 and stopped reading kind 1 entirely. The move was right. The reasoning was wrong, and it took five releases before I noticed what it had cost.
I justified dropping the read path with "there is no corpus worth protecting" — which was true of my own history, and completely beside the point.
{kinds:[1], "#r":[url]} does not only return comments made with my extension. An r-tagged note is how any Nostr client links a note to a URL. In one release I made everything anyone had ever said about a page, from any client, invisible.
It surfaced as something else first. Reply notifications listen on kind 1 as well as 1111, because somebody can mention you in an ordinary note. So the badge would light up for a mention the thread had no way to display. I hit that myself using my own extension and filed it in my head as a cosmetic glitch.
Kind 1 notes are read again now, marked as notes rather than comments. Writing is still 1111 only. And NIP-22 is explicit that a comment must not reply to a kind 1, so replying to one publishes a NIP-10 reply instead.
If you are planning the same migration: separate the two decisions. What you write is about being a good citizen of the protocol. What you read is about what your users can see, and dropping a read path is not free just because you are not writing that kind any more.
Joey (NostrComments)
slurpnc@coinos.io
npub1ewxm...ds9e
I build NostrComments — a browser extension that adds a censorship-resistant comment section to every website, powered by Nostr. Free and open source, always.
If your extension puts a private key in the DOM, a script on the page can read it.
I found this in my own, so this is not a hypothetical.
The panel lives in a shadow root. Shadow roots feel private, and they are not:
{mode:"open"} means any script on the page reaches it with host.shadowRoot. Mine
filled the key field the moment Settings was opened — not when the key was
revealed — and never cleared it. Open Settings once and the key sat there,
readable, for the rest of the page's life.
It also survived deleting the identity. "Show private key" would hand back the
key you had just destroyed.
Two more of the same shape, once I went looking: the password typed to encrypt
the key was blanked when its dialog opened rather than when it closed, and a
pasted nsec stayed in the import field after cancelling.
The fix is not a closed shadow root. A page script that runs before yours can
hook Element.prototype.attachShadow and keep the reference anyway. The fix is
that the secret is never in the DOM: hold it in the content script's own world,
put it in the input only while it is on screen, clear it on hide.
I demonstrated it from page context before fixing it, then wrote a test that
does the same, so it stays fixed.
If you're implementing NIP-09 deletion in a Nostr client, there's one check
that's easy to miss:
A kind 5 only counts when it's signed by the author of the event it targets.
Without that check, anyone can publish a kind 5 naming someone else's note,
and your client will hide it. You've built a censorship button and handed it
to everyone.
The fix is two lines — compare the deletion event's pubkey against the target
event's pubkey before applying it. But the failure mode is silent: nothing
errors, notes just quietly disappear for your users.
Ran into this adding deletion to NostrComments. Ended up writing a test for
it rather than trusting a careful reading.