If your browser extension draws its UI inside the page, check this one. I had it wrong for months.
My comment panel lives in an open shadow root in the page, so any script on the page can reach every control in it. I guarded the ones that matter — granting consent, adding a relay — by checking the event's isTrusted, which only the browser can set. That felt like enough.
It is not, and I only found out because I tried to break it myself instead of assuming.
A page can skip the event entirely: take the element, call its onclick directly, and hand it an object it made up. isTrusted is just a property on that object. It can also act while a real click of yours is being delivered elsewhere in the panel, which is long enough for a naive "was that trusted" flag to still be true.
Against my own build that meant a page could open settings, press "Show private key" and read the nsec straight out of the field. And separately, type a comment and post it under my key. Without me clicking anything.
What works instead is to never trust what the caller passed. A capture listener on the shadow root records which element a real gesture is activating, in a closure the page cannot reach, and each sensitive handler asks only whether that element is itself. Dispatching an event sets nothing. Calling a handler sends no event at all. Piggybacking on a real click leaves some other element as the target.
It is still a mitigation and not a boundary, and I would rather say so: the panel is in the page's DOM, so a page can read the thread, read what I am typing, and read a key I choose to display while I am on it. Moving those controls onto the extension's own surface is the actual fix, and that is the next thing I build.
Fixed in 23.3.1 — out on Firefox, in review at Chrome.
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 — and Attest, a Nostr signer for Firefox that holds your key so a website never has to, and asks before it signs anything. Free and open source, always.
NostrComments for Firefox:
https://addons.mozilla.org/en-US/firefox/addon/nostrcomments/
NostrComments for Chrome:
https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido
Attest for Firefox:
https://addons.mozilla.org/firefox/addon/attest/
Monero: 87aDTPD9HQx2QenKsS7MvHDdqsziFPD7UB37X6G5XVXc2ZPhAs8DdEKUPYJijVcRjj1gU5KvxLCTfWUKWqrd1D5o8uw5EpM
I often feel like an outsider on nostr. Do people actually read this?
Ever wanted to reply to an article on the internet that has comments switched off? 👀
I built NostrComments for that. It's a browser extension that gives every web page a comment section, powered by Nostr.
Here's the idea: your comment doesn't live on the website. It lives on Nostr relays, linked to the page's address. So the website can't delete it, and anyone else with NostrComments who visits that page can see it and reply.
A few nice things:
✍ Use your own Nostr key, or keep it safe in a signer like Alby, Attest or nos2x
🤝 Comments on web pages made with other Nostr apps, like Amethyst and Ditto, show up too
🔌 No company server in the middle. You pick your relays
🙋 It stays quiet until you say yes, and you can turn it off for any site
⚡ Vote, mute and zap, right in the panel
Curious but don't want to install anything? Read the comments on any page here:
Quick demo (66 sec):
🦊 Firefox:
🌐 Chrome: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido
💻 Open source:
It's still early, so most pages have no comments yet. If you try it, leave one! That's how every conversation starts 💜
#nostr #grownostr #opensource #foss #privacy #nip22
Read a NostrComments thread
Read the comment thread attached to any web page, without installing anything.
Client Challenge
GitHub
GitHub - briskness-byte/NostrComments: The comment section no website can close. A thread on every page, kept on Nostr relays. Browser extension for Firefox and Chrome. No account, no server, no tracking.
The comment section no website can close. A thread on every page, kept on Nostr relays. Browser extension for Firefox and Chrome. No account, no se...
NostrComments has a proper demo now — 66 seconds, no sales pitch:
A comment thread on any page, attached to the page's address and stored on relays instead of on the site. The clip shows it on a Nature article with no comment section, the same thread appearing in a second browser under a different key, and then a paywalled paper, a corporate announcement, an EU page and a post on X.
Also out: 23.2.2, where the toolbar button opens and closes the panel. That matters because a page can delete the floating button — the toolbar is outside its reach.
Firefox:
Chrome: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido
Client Challenge
Attest is a Nostr signer for Firefox: it holds your key and asks before anything is signed.
What's new in recent releases:
• Protect your key with a passphrase, not just a 4–6 digit PIN. A short PIN stops someone using your browser; it does not stop someone who copies your Firefox profile, and the extension now says so plainly instead of implying otherwise.
• Permissions are tied to a site's full address, https or http. A site you allowed over https is no longer also allowed over plain http.
• Each site keeps one decision per kind of permission, so refusing something briefly no longer wipes a permission you granted permanently.
• The options page shows what is actually there: no empty key field for a key that is encrypted, and adding another key happens where you'd look for it.
Open source, reproducible build, and the release refuses to ship if the tests fail or if the code that handles your keys doesn't type-check.
https://github.com/briskness-byte/attest
Client Challenge
NostrComments 23.2.0 is out 🧵
It now works on sites that used to show an empty panel. Some sites (x.com is the best-known) use a strict Content-Security-Policy that blocks connections to anywhere they haven't listed, and Firefox applied that rule to the extension too, so no relay could be reached. The extension now opens its relay connections from its own background, where the site has no say.
Also new:
• a switch to leave the "client" tag off your comments
• on Firefox, the welcome screen now suggests Attest instead of nos2x-fox, which has a public flaw that lets a website read the PIN protecting your key. Attest is my fork with that fixed.
What it is: a comment thread on any URL, stored on relays instead of on the site, so the site owner can't delete it. Comments are NIP-22 (kind 1111), so other clients can read them.
Firefox:
Chrome/Brave/Edge: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido
Source:
No server, no account, no tracking. Feedback welcome, here or in the thread on any page.
#nostr #privacy 
Client Challenge
GitHub
GitHub - briskness-byte/NostrComments: The comment section no website can close. A thread on every page, kept on Nostr relays. Browser extension for Firefox and Chrome. No account, no server, no tracking.
The comment section no website can close. A thread on every page, kept on Nostr relays. Browser extension for Firefox and Chrome. No account, no se...

A browser extension can be talked to by two very different things: its own pages, and a content script sitting in somebody's web page. Telling them apart is the whole security boundary, and the obvious way to do it is wrong.
`sender.tab` looks like the answer. It is not. An options page opened with tabs.create() has a tab like any web page does, so checking for one makes the extension refuse its own UI — and it fails silently, because the message goes out, comes back refused, and nothing on screen says why. A button that does nothing.
`sender.url` against runtime.getURL('') is the one that holds. A content script always reports the address of the page it was injected into, never a moz-extension:// one, and content scripts do not run on extension pages.
Cost me a release before I found it.
I hand-rolled secp256k1 and BIP-340 Schnorr for a browser extension, and I would like someone who does this properly to read it.
Why hand-rolled: the extension ships with no build step and no dependencies, so the file you install is the file in the repository, comparable line for line. A library would have cost that. Whether the trade was worth it is part of what I am asking.
What I have already done. It passes all nineteen official BIP-340 test vectors, including the ten that must be rejected — public key off the curve, sig[0:32] not an x coordinate, s equal to the curve order, sG - eP at infinity. I mutation-tested that suite too: removing the even-y check on R is caught by exactly one assertion in the entire project, vector #6, and by nothing else.
What I already know is weak. The scalar multiplication is a plain double-and-add branching on secret bits, so it runs in variable time. I do not think it is reachable from a hostile page — a content script's signing cannot be triggered or observed from the page — but I would rather say it than have it pointed out. And BigInt is not constant time in any JS engine, so this is not something I can fully fix in this language.
What testing cannot tell me: whether the field arithmetic is right. Vectors prove the implementation behaves on inputs somebody thought to write down. They say nothing about the modular inverse.
It is forty lines. I am not asking anyone for a free audit — just for eyes. I would rather find out now than after somebody depends on it.
https://github.com/briskness-byte/NostrComments/blob/v23.1.0/NostrComments-Chrome/content.js#L56-L95
I was about to post a measurement. I re-ran it first, which I do, and it fell apart in my hands.
The claim: across six news domains, the notes on Nostr that link to news articles are essentially one account. 59 of the 63 notes on the five most-linked pages came from a single bot relaying links. Four distinct authors in total. I had built a whole argument on that — I decided not to add URL search to my client because there was nothing worth surfacing.
Redone sixteen days later: 3,050 distinct article pages, 17 distinct authors on the five most-linked, largest single author 16%. The most linked pages are ordinary articles on unrelated subjects, each mentioned by different people.
Both runs used the same three search relays that answer a NIP-50 filter at all, of seven tried. The difference between them is one function call.
The corpus was there the whole time. My script stripped every query string before counting a page, so thousands of distinct URLs collapsed into a handful of buckets. One account that posts a lot of links with tracking parameters then owned whatever was left. The bot was not the corpus. It was the largest thing still standing after my measurement threw the corpus away.
The part I find hardest to be relaxed about: my own client already has a URL normaliser that gets this right. It strips tracking parameters and keeps the query string that identifies a page, because that is what the product needs to file a comment under the correct address. The measurement did not use it. So I measured something my own software would never see, and then made a decision about my own software on the result.
The decision happens to survive — I still would not build that search, for reasons about relay reliability and about not sending the page you are reading to an index you did not choose. But it survived by luck, not by reasoning, and those are not the same thing.
If you measure your own product: measure with the code your product runs. Anything else and you are describing a system nobody uses.
Hello. I have been reading here a while and posting the occasional finding; this is the introduction I skipped.
I build a comment layer for web pages, on Nostr. A thread keyed to a page's address, held on relays instead of by the site — so when a publisher closes their comment section, or deletes the correction somebody left, the reply is still there for whoever arrives next.
It started with scam sites. An operator removes warnings from their own page within minutes. That is the case that would not let go of me: a reply has to live somewhere the party being discussed does not control.
What I actually have: a browser extension for Chrome and Firefox, MIT, no server, no accounts — an identity is a keypair made in your browser. And almost no users. I would rather say that than imply otherwise. The layer exists before the audience does.
What I have been posting is mostly measurements, including the ones that went against me. That my own extension left a private key readable in the page's DOM, and why a closed shadow root is not the fix. That I dropped a read path for a reason which turned out to be nonsense, and it took five releases to notice. That NIP-09 deletion has an author check most clients skip. There is more of that coming.
I am here for the people who build the plumbing. If you work on relays, clients or NIPs, I would like to know you.
#introductions
Lets discuss articles freely with the NostrComments browser extension:
https://edition.cnn.com/

NostrComments — the comment section no website can close
A thread on every web page, kept on Nostr relays, not on the site. No account, no server, no tracking.
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.
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.