NostrClay demo B · approach B, the event-composed page
A session board with three people writing to it and no server anywhere.
The file you just downloaded contains none of the sessions. They arrive a moment later as
signed, addressable kind 35129 events, are verified in your browser, sanitised,
and applied into the marked regions below.
One region, one row per contributor. A row is an addressable event: the relay files it
under (kind, pubkey, d-tag), so republishing with the same d replaces
it in place, forever, and nobody else's key can touch it.
| Slot | Session | Speaker | Signed by |
|---|---|---|---|
| — | The programme is not in this file | Every session below is one signed event, fetched from relays at view time. | nobody yet |
| — | If you are reading this, the relays stayed quiet | Either JavaScript is off, or the relays in nc:relays are unreachable from here. |
— |
Your row ·
Three <td> cells: slot, session, speaker. Publish signs one event and pushes it to
the relays; every browser on this page re-renders. Retract files a kind 5 deletion for
the same address.
Signed in, not on the list
Your key is not in nc:writers, so nc.publishRegion() refuses to sign.
Nothing stops you publishing a 35129 event addressed at this page by hand, though —
relays take anything. Try it, and watch every reader's copy of this page drop it on the floor.
Access control is not a permission bit on a server. It is the subscription filter plus a
signature check: an event only becomes DOM if its signature verifies, its a tag names
this document, and its author is on the list below.
Writer list (from nc:writers)
The filter this page actually subscribes with
…
Add nc:open to <html> and the authors line disappears:
anyone's signature is accepted and the page becomes a wiki. The markup is still sanitised —
nc:open relaxes who, never what. This demo ships with the
whitelist, so the events on the right are refused.
Events addressed here from keys not on the list
These are real events, sitting on the same relays, naming this exact document. They are on
your machine right now — this panel had to ask for them with a second, deliberately unfiltered
subscription. The page's own subscription never requests them, and
accept() would refuse them anyway. That is the entire access-control model.
This is approach B's bill, and it is due immediately. The bytes the gateway serves are the shell;
the board is assembled afterwards, in a browser, over websockets. Below: the served file, fetched
back from location.href and measured against the DOM you are looking at.
Served bytes vs composed DOM
measuring…
The rows region, as served
…
The fix, when you want it
nc.compact() (owner only) folds every applied event into the document, stamps
nc:since as a watermark, and rewrites the file through approach A. After that the
static bytes are the board, the layer starts empty, and new events pile up on top again.
Demo B deliberately never compacts, so you can see the gap it closes.
A writer is trusted to own a row, not to be nice. One of the whitelisted keys has published a region event whose content is a small pile of XSS. It is on the relays verbatim; it is inert here.
As signed and stored on the relay
…
After nc.sanitize(), as inserted into the DOM
…
Rendered, live, in this page
No hazard event has arrived yet.
The allow-list is tags and attributes, not a blocklist: script, style,
iframe, form and friends are dropped, every on* handler
goes, and a URL survives only if it starts https:, http:,
mailto:, # or /. publishRegion() sanitises on
the way out too, so a compliant client never even signs the dirty version — but that is a
courtesy, not a defence. The defence is that your browser sanitises on the way in.
This document is addressed as …. Every region
event carries that address in an a tag and its own d tag of the form
<address>#<region>. The runtime subscribes once, verifies each signature
with verifyEvent, keeps the newest event per (author, d), sorts by
created_at, and writes them into [nc:region] hosts — stamping
nc:by, nc:d and nc:at on each item, which is where the
provenance column above comes from. No coordinator, so ordering is by the clock in each
signer's pocket: edit your row and it moves to the bottom of the board.
This page's configuration
…