docs(nestsClient/specs): EGG-00..EGG-12 — Extensible Gossip Guidelines
Wire-protocol specs for nostrnests-style audio rooms, in the style of Nostr NIPs and Blossom BUDs. Each spec documents one self-contained capability that a client or relay can implement; two compliant peers implementing the same set of EGGs round-trip without further coordination. Layout: README.md cover, status table, conformance levels EGG-01.md Room event (kind:30312) required EGG-02.md Auth + WebTransport handshake required EGG-03.md Audio plane (moq-lite) required EGG-04.md Presence (kind:10312) required EGG-05.md In-room chat (kind:1311) optional EGG-06.md Reactions (kind:7) optional EGG-07.md Roles & moderation (kind:4312) optional EGG-08.md Scheduling (status=planned) optional EGG-09.md User server list (kind:10112) optional EGG-10.md Theming (c/f/bg tags) decorative EGG-11.md Recording decorative EGG-12.md Catalog track (catalog.json) optional Conformance levels (Listener / Speaker / Host) defined in the README so a deployment can declare "we implement EGG-01..EGG-04" and other peers know exactly what to expect. Each spec follows the same shape (Summary / Wire format / Behavior numbered MUST/SHOULD/MAY rules / Example / Compatibility) and fits on a single printed page. Wire formats are documented exactly as nostrnests + amethyst implement them on this branch — no hypothetical capabilities, no "future" tags without an EGG number.
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
# EGG-01: Room event (`kind:30312`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: NIP-01`
|
||||||
|
`category: required`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A nests audio room is a NIP-53 addressable event of `kind:30312`. The author
|
||||||
|
is the host. Subscribers read this event to learn who runs the room, where the
|
||||||
|
audio plane lives, who else is invited, and whether the room is currently live.
|
||||||
|
|
||||||
|
Every other EGG layers on top of this event.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "<host pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["d", "<room id>"],
|
||||||
|
["room", "<room name>"],
|
||||||
|
["summary", "<one-line description>"],
|
||||||
|
["image", "<optional cover image URL>"],
|
||||||
|
["status", "open" | "private" | "closed" | "planned"],
|
||||||
|
["service", "<https URL of moq-auth sidecar>"],
|
||||||
|
["endpoint", "<https URL of moq-relay WebTransport>"],
|
||||||
|
["relays", "<wss relay 1>", "<wss relay 2>", ...],
|
||||||
|
["p", "<pubkey>", "<relay hint>", "host" | "admin" | "speaker"],
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `d` tag is the room id; `(pubkey, kind, d)` is the addressable identity.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Hosts MUST emit exactly one `kind:30312` event per room. Updating the room
|
||||||
|
(rename, status change, role grants) MUST re-publish the same `d` tag with
|
||||||
|
a higher `created_at`.
|
||||||
|
2. The `room`, `status`, `service`, and `endpoint` tags MUST be present. A
|
||||||
|
client receiving an event without all four MUST treat the room as
|
||||||
|
un-joinable.
|
||||||
|
3. The `service` value MUST be the base URL of an EGG-02 auth sidecar (do
|
||||||
|
not include the `/auth` suffix).
|
||||||
|
4. The `endpoint` value MUST be the base URL of a WebTransport-capable
|
||||||
|
moq-relay implementing EGG-03.
|
||||||
|
5. The `p` tag MUST list the host as the first participant. Additional
|
||||||
|
`p` tags grant roles (EGG-07).
|
||||||
|
6. Status semantics:
|
||||||
|
- `open` — room is live, anyone can join.
|
||||||
|
- `private` — room is live, an out-of-band invitation gate applies (relay
|
||||||
|
enforces; client behavior identical to `open`).
|
||||||
|
- `closed` — room is over, audio plane SHOULD be torn down server-side.
|
||||||
|
- `planned` — room hasn't started; see EGG-08.
|
||||||
|
7. A host MAY treat a room as auto-closed after 8 h of `created_at` staleness
|
||||||
|
even if the published status is still `open`/`private`. Receivers SHOULD do
|
||||||
|
the same to avoid stale rooms hanging at the top of the live UI.
|
||||||
|
8. An empty `content` field is REQUIRED. Future EGGs MAY define structured
|
||||||
|
content; until then, peers MUST ignore non-empty content rather than
|
||||||
|
rejecting the event.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"created_at": 1714003200,
|
||||||
|
"tags": [
|
||||||
|
["d", "office-hours-2026-04"],
|
||||||
|
["room", "Office Hours"],
|
||||||
|
["summary", "Weekly Q&A"],
|
||||||
|
["status", "open"],
|
||||||
|
["service", "https://moq.nostrnests.com"],
|
||||||
|
["endpoint", "https://moq.nostrnests.com"],
|
||||||
|
["p", "abc...host", "wss://relay.example", "host"],
|
||||||
|
["p", "def...co", "wss://relay.example", "speaker"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Peers without EGG-01 cannot interpret any other EGG. EGG-04, EGG-05, EGG-06,
|
||||||
|
EGG-07, and EGG-12 reference rooms by the `(kind, pubkey, d)` tuple defined
|
||||||
|
here.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# EGG-02: Auth & WebTransport handshake
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01, NIP-98`
|
||||||
|
`category: required`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
To open the audio plane (EGG-03), a peer first proves identity to a per-room
|
||||||
|
auth sidecar (moq-auth) using a NIP-98 HTTP signature, receives a short-lived
|
||||||
|
JWT, then opens a WebTransport session against the moq-relay carrying the JWT
|
||||||
|
in the URL.
|
||||||
|
|
||||||
|
Two separate URLs are involved: the `service` tag from EGG-01 is the auth
|
||||||
|
sidecar; the `endpoint` tag is the relay.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
### Step 1 — token request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST <service>/auth
|
||||||
|
Authorization: Nostr <base64(NIP-98 kind:27235 event)>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "namespace": "nests/<kind>:<host_pubkey_hex>:<room_d>",
|
||||||
|
"publish": <boolean> }
|
||||||
|
```
|
||||||
|
|
||||||
|
`<kind>` is `30312` for nests audio rooms (EGG-01).
|
||||||
|
|
||||||
|
The NIP-98 event MUST bind to this exact URL, method `POST`, and the SHA-256
|
||||||
|
hash of the request body.
|
||||||
|
|
||||||
|
### Step 2 — token response
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "token": "<jwt>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The JWT carries (at minimum):
|
||||||
|
|
||||||
|
| claim | meaning |
|
||||||
|
|---------|---------------------------------------------------------------|
|
||||||
|
| `root` | The exact `namespace` echoed back. Authorisation is scoped here. |
|
||||||
|
| `get` | Read-allowed sub-paths; for nests this is `[""]` (any). |
|
||||||
|
| `put` | Publish-allowed sub-paths. For listeners: `[]`. For speakers: `[<own pubkey hex>]`. |
|
||||||
|
| `iat` | Unix seconds; issuance. |
|
||||||
|
| `exp` | Unix seconds; expiry. MUST be `iat + 600` for nests today. |
|
||||||
|
|
||||||
|
### Step 3 — WebTransport CONNECT
|
||||||
|
|
||||||
|
```
|
||||||
|
:method = CONNECT
|
||||||
|
:protocol = webtransport
|
||||||
|
:scheme = https
|
||||||
|
:authority = <host:port from `endpoint`>
|
||||||
|
:path = /<namespace>?jwt=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
The relay reads the JWT from the `?jwt=` query string. No `Authorization`
|
||||||
|
header is used at this step.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Listening peers MUST request `publish: false`. Speaking peers MUST request
|
||||||
|
`publish: true` AND the JWT's `put` claim MUST list the speaker's own
|
||||||
|
pubkey hex.
|
||||||
|
2. The auth sidecar's TLS certificate MUST be a publicly-trusted chain. The
|
||||||
|
relay's TLS chain MAY be self-signed in development; production deployments
|
||||||
|
MUST use a public chain.
|
||||||
|
3. The auth sidecar SHOULD reject any request whose NIP-98 `created_at` is
|
||||||
|
more than 60 s in the past or future.
|
||||||
|
4. The JWT lifetime is fixed at 600 s. There is no refresh endpoint. A client
|
||||||
|
that needs a longer session MUST mint a fresh token and open a new
|
||||||
|
WebTransport session before the old token expires; see the deployment-side
|
||||||
|
commentary in `nestsClient/plans/`.
|
||||||
|
5. The relay MUST close the WebTransport session within 30 s of `exp`. A peer
|
||||||
|
receiving an unexpected close MUST be prepared to mint a fresh token and
|
||||||
|
reconnect.
|
||||||
|
6. The relay MUST NOT trust the WebTransport authority for authorisation —
|
||||||
|
only the JWT's `root` claim. A token issued for namespace A MUST NOT be
|
||||||
|
accepted on a session opened against namespace B.
|
||||||
|
7. A peer MUST NOT log the JWT or include it in error reports. The token is a
|
||||||
|
bearer credential.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```
|
||||||
|
> POST https://moq.nostrnests.com/auth
|
||||||
|
> Authorization: Nostr eyJ...kind27235...
|
||||||
|
> Content-Type: application/json
|
||||||
|
>
|
||||||
|
> {"namespace":"nests/30312:abchost:office-hours-2026-04","publish":false}
|
||||||
|
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Content-Type: application/json
|
||||||
|
<
|
||||||
|
< {"token":"eyJhbGc..."}
|
||||||
|
|
||||||
|
> CONNECT :path=/nests/30312:abchost:office-hours-2026-04?jwt=eyJhbGc...
|
||||||
|
< 200 OK (WebTransport session established)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-03 (audio plane) operates inside the WebTransport session this EGG opens.
|
||||||
|
EGG-12 (catalog) shares the same session.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# EGG-03: Audio plane (moq-lite)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-02`
|
||||||
|
`category: required`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Audio is carried over [moq-lite](https://github.com/kixelated/moq) Lite-03 on
|
||||||
|
top of the WebTransport session opened in EGG-02. Each speaker publishes one
|
||||||
|
broadcast keyed by their pubkey hex, on a single track named `audio/data`,
|
||||||
|
encoded as Opus.
|
||||||
|
|
||||||
|
This EGG defines the broadcast/track naming convention and the audio codec
|
||||||
|
parameters. It does not redefine moq-lite framing — refer to the upstream
|
||||||
|
spec for the wire layout of `Subscribe`, `Announce`, `Group`, and `Frame`
|
||||||
|
control messages.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
### Speaker → relay (publish)
|
||||||
|
|
||||||
|
A speaker MUST send a moq-lite `Announce` with:
|
||||||
|
|
||||||
|
```
|
||||||
|
prefix = "" (empty — relative to the JWT's `root` namespace)
|
||||||
|
suffix = <pubkey hex>
|
||||||
|
status = Active
|
||||||
|
```
|
||||||
|
|
||||||
|
After the announce, the speaker opens a unidirectional QUIC stream per group,
|
||||||
|
prefixed with a moq-lite `Group` header followed by `Frame`s carrying Opus
|
||||||
|
payloads.
|
||||||
|
|
||||||
|
### Listener → relay (subscribe)
|
||||||
|
|
||||||
|
A listener MUST send a moq-lite `Subscribe` with:
|
||||||
|
|
||||||
|
```
|
||||||
|
broadcast = <speaker pubkey hex>
|
||||||
|
track = "audio/data"
|
||||||
|
priority = 128 (recommended)
|
||||||
|
ordered = true
|
||||||
|
maxLatency = 0 (unlimited)
|
||||||
|
```
|
||||||
|
|
||||||
|
The relay forwards each subsequent `Group` and its `Frame`s to the listener
|
||||||
|
in subscribe-id order.
|
||||||
|
|
||||||
|
### Codec
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------------|---------------------------------------|
|
||||||
|
| Codec | Opus (RFC 6716) |
|
||||||
|
| Sample rate | 48 kHz |
|
||||||
|
| Channels | 1 (mono) |
|
||||||
|
| Frame duration | 20 ms (960 samples) |
|
||||||
|
| Bit-rate target | 32 kbit/s (configurable, no upper cap)|
|
||||||
|
| VBR | Allowed |
|
||||||
|
|
||||||
|
A new `Group` MUST be opened on every Opus reset (e.g. mute/unmute).
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. A speaker MUST emit an `Announce` with `status=Active` immediately after
|
||||||
|
the WebTransport session reaches a usable state, and emit `status=Ended`
|
||||||
|
when stopping the broadcast cleanly.
|
||||||
|
2. A speaker MUST cap their broadcast to one concurrent `audio/data` track
|
||||||
|
per session. Multi-quality stacks are reserved for a future EGG.
|
||||||
|
3. Listeners MUST tolerate gaps in the group sequence (frames dropped due to
|
||||||
|
network loss). They MUST NOT request retransmits.
|
||||||
|
4. Listeners SHOULD subscribe with `ordered=true`. Out-of-order delivery
|
||||||
|
would re-introduce reorderable jitter the Opus decoder is not equipped to
|
||||||
|
handle.
|
||||||
|
5. A speaker MUST encode in mono. Stereo broadcasts are reserved for a future
|
||||||
|
EGG.
|
||||||
|
6. Listeners MUST NOT subscribe to their own broadcast (would create an audio
|
||||||
|
loopback through the relay).
|
||||||
|
7. The relay MUST drop a publishing peer's `Announce` and any subsequent
|
||||||
|
stream data if the JWT's `put` claim does not contain `<pubkey hex>`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
A two-speaker room:
|
||||||
|
|
||||||
|
```
|
||||||
|
A's session: ANNOUNCE prefix="" suffix="speakerA" status=Active
|
||||||
|
GROUP subscribe-id=…, sequence=0
|
||||||
|
FRAME <opus payload, 20ms>
|
||||||
|
FRAME <opus payload, 20ms>
|
||||||
|
…
|
||||||
|
|
||||||
|
Listener's session:
|
||||||
|
SUBSCRIBE broadcast="speakerA" track="audio/data"
|
||||||
|
← GROUP sequence=0
|
||||||
|
← FRAME <opus payload, 20ms>
|
||||||
|
← …
|
||||||
|
SUBSCRIBE broadcast="speakerB" track="audio/data"
|
||||||
|
← GROUP sequence=0
|
||||||
|
← FRAME <opus payload, 20ms>
|
||||||
|
← …
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-12 adds an OPTIONAL `catalog.json` track per broadcast carrying codec
|
||||||
|
metadata. EGG-12 MUST NOT change the `audio/data` track parameters defined
|
||||||
|
here.
|
||||||
|
|
||||||
|
A future "video plane" EGG MAY introduce additional tracks; their names MUST
|
||||||
|
NOT collide with `audio/data` or `catalog.json`.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# EGG-04: Presence (`kind:10312`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: required`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Each participant in a room periodically broadcasts a `kind:10312` event
|
||||||
|
declaring their state: which room they are in, whether their hand is raised,
|
||||||
|
whether they are muted, whether they are actively pushing audio, and whether
|
||||||
|
they currently hold a stage seat.
|
||||||
|
|
||||||
|
Presence is the source of truth for the audience-side participant grid, the
|
||||||
|
hand-raise queue, the listener counter, and the home-screen "live" bubble.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
`kind:10312` is a NIP-01 *replaceable* event with a fixed `d`-tag (one
|
||||||
|
presence-state per pubkey, regardless of how many rooms a user joins; see
|
||||||
|
"Behavior" below).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 10312,
|
||||||
|
"pubkey": "<participant pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
|
||||||
|
["hand", "0" | "1"],
|
||||||
|
["muted", "0" | "1"],
|
||||||
|
["publishing", "0" | "1"],
|
||||||
|
["onstage", "0" | "1"],
|
||||||
|
["alt", "Room Presence tag"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `a` tag is REQUIRED and points at the room defined in EGG-01.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. A participant MUST emit a `kind:10312` event:
|
||||||
|
- on entering the room,
|
||||||
|
- every 30 s while in the room,
|
||||||
|
- immediately when any flag (`hand`, `muted`, `publishing`, `onstage`)
|
||||||
|
changes,
|
||||||
|
- one final time on leaving, with `publishing="0"` and `onstage="0"`.
|
||||||
|
2. Receivers MUST treat a participant as departed if no presence has been
|
||||||
|
observed in `> 6 minutes`. (One missed heartbeat plus a 5 minute
|
||||||
|
tolerance window.)
|
||||||
|
3. The `hand` flag is the participant's request-to-speak. Hosts and admins
|
||||||
|
read it to populate a queue (EGG-07).
|
||||||
|
4. The `muted` flag is set ONLY by speakers who are also publishing. Pure
|
||||||
|
audience members SHOULD omit it.
|
||||||
|
5. The `publishing` flag means "I currently have an open `audio/data`
|
||||||
|
broadcast on the relay" (EGG-03). It MUST be `0` whenever the speaker
|
||||||
|
has stopped publishing, regardless of mute state.
|
||||||
|
6. The `onstage` flag means "I currently hold a speaker slot in the room's
|
||||||
|
`p`-tag list (EGG-01) AND I have not voluntarily stepped off". A
|
||||||
|
speaker who steps off the stage without losing the role tag MUST emit
|
||||||
|
`onstage="0"`.
|
||||||
|
7. The `d`-tag MUST be the FIXED string `nests-room-presence` (chosen so
|
||||||
|
that one presence event represents a participant's CURRENT room across
|
||||||
|
all clients). A participant MUST NOT be in two rooms simultaneously.
|
||||||
|
8. Every flag MUST be present in every emitted event. Receivers MAY assume
|
||||||
|
omitted flags default to `0`, but emitters that omit are non-conformant.
|
||||||
|
9. Hosts and clients displaying the participant grid MUST hide presences
|
||||||
|
from `kind:10312` events whose `created_at` is older than the staleness
|
||||||
|
window in rule (2).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 10312,
|
||||||
|
"pubkey": "def...co",
|
||||||
|
"created_at": 1714003231,
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:abchost:office-hours-2026-04", "wss://relay.example"],
|
||||||
|
["hand", "0"],
|
||||||
|
["muted", "0"],
|
||||||
|
["publishing", "1"],
|
||||||
|
["onstage", "1"],
|
||||||
|
["alt", "Room Presence tag"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-07 (moderation) reads `hand` to drive the host's promote queue.
|
||||||
|
EGG-03 (audio plane) is the source of truth for whether a speaker IS
|
||||||
|
broadcasting; presence's `publishing` flag is a Nostr-side reflection that
|
||||||
|
clients use as a hint when they cannot subscribe to the audio plane (e.g.
|
||||||
|
listeners on weak networks rendering the home bubble).
|
||||||
|
|
||||||
|
The `kind:10312` shape here intentionally mirrors `kind:10311` from
|
||||||
|
NIP-53 streaming so that a generic Nostr client can render audience
|
||||||
|
indicators for both protocols without code-paths.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# EGG-05: In-room chat (`kind:1311`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
In-room text chat reuses NIP-53's `kind:1311` "live activities chat message"
|
||||||
|
event, addressed to the room via an `a`-tag. No new event kind, no new
|
||||||
|
relay convention.
|
||||||
|
|
||||||
|
A peer that does not implement EGG-05 simply does not render or emit chat;
|
||||||
|
all other EGGs continue to interop.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 1311,
|
||||||
|
"pubkey": "<author pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"]
|
||||||
|
],
|
||||||
|
"content": "<plaintext message>",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `content` field carries the plaintext (UTF-8) message.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Senders MUST emit chat events with `kind:1311` and exactly one `a`-tag
|
||||||
|
pointing at the room (EGG-01).
|
||||||
|
2. Receivers MUST subscribe to relays declared on the room's `relays` tag
|
||||||
|
(EGG-01) with filter `{kinds:[1311], "#a":["30312:<host>:<d>"]}`.
|
||||||
|
3. Receivers MUST de-duplicate by event id when the same message arrives
|
||||||
|
from multiple relays.
|
||||||
|
4. Receivers MUST sort the chat panel by `created_at` ascending so the
|
||||||
|
newest message is at the bottom.
|
||||||
|
5. Senders MUST NOT include private content. The chat plane is public;
|
||||||
|
any peer subscribed to the room's relays sees every message.
|
||||||
|
6. Senders MAY include other NIP tags (`p` mentions, `e` quotes, `q`
|
||||||
|
embeds). Receivers MAY render these per the relevant NIP; lacking
|
||||||
|
support MUST NOT cause the message to be hidden.
|
||||||
|
7. A peer SHOULD NOT publish chat events while `status` of the
|
||||||
|
`kind:30312` is `closed` — but receivers MUST tolerate them gracefully
|
||||||
|
in case of a race against the host's close.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 1311,
|
||||||
|
"pubkey": "def...co",
|
||||||
|
"created_at": 1714003205,
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:abchost:office-hours-2026-04", "wss://relay.example"]
|
||||||
|
],
|
||||||
|
"content": "Hi everyone, glad to be here!",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-05 events flow on Nostr relays, not the audio plane. A relay that
|
||||||
|
implements only the audio half of nests can be paired with any NIP-01
|
||||||
|
relay to provide the chat half.
|
||||||
|
|
||||||
|
EGG-06 (reactions) targets `kind:7` events at the same `a`-tag. Chat and
|
||||||
|
reactions are independent: a peer MAY render reactions without rendering
|
||||||
|
chat or vice versa.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# EGG-06: Reactions (`kind:7`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Audience-side ephemeral feedback (👏 ❤️ 🎉 ...) is carried on standard
|
||||||
|
NIP-25 `kind:7` reaction events, scoped to the room via an `a`-tag.
|
||||||
|
Clients render these as a short-lived floating overlay over the
|
||||||
|
participant grid.
|
||||||
|
|
||||||
|
A reaction is NOT durable state. Clients SHOULD NOT page through
|
||||||
|
historical reactions; only the recent window matters.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7,
|
||||||
|
"pubkey": "<reactor pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
|
||||||
|
["p", "<target pubkey hex>"]?,
|
||||||
|
["emoji", "<shortcode>", "<image url>"]?
|
||||||
|
],
|
||||||
|
"content": "<emoji glyph or :shortcode:>",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`content` carries the reaction itself. Standard Unicode emoji glyphs are
|
||||||
|
used directly. Custom emoji follow NIP-30: `content = ":shortcode:"` with
|
||||||
|
a sibling `["emoji", shortcode, url]` tag.
|
||||||
|
|
||||||
|
A reaction MAY include a `["p", target]` tag scoping the reaction to a
|
||||||
|
specific speaker (renders over their avatar). With no `p` tag, the reaction
|
||||||
|
is room-wide (renders centered on the room canvas).
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Reactors MUST emit `kind:7` events with at least one `a`-tag matching
|
||||||
|
EGG-01 and a non-empty `content` field.
|
||||||
|
2. Receivers MUST apply a 30-second sliding window: a reaction is rendered
|
||||||
|
from the moment it is observed until 30 seconds after its `created_at`,
|
||||||
|
then dropped from the floating overlay.
|
||||||
|
3. Receivers SHOULD throttle their reaction-overlay updates to at most 4 Hz
|
||||||
|
(250 ms minimum between repaints). At ~30 reactions per second a naive
|
||||||
|
recomposition can starve the audio decoder.
|
||||||
|
4. Receivers MUST tolerate unknown `content` values (e.g. a Unicode glyph
|
||||||
|
without a font fallback) by falling back to a generic indicator rather
|
||||||
|
than dropping the reaction.
|
||||||
|
5. Receivers MUST NOT auto-fetch the `["emoji", _, url]` image at the
|
||||||
|
moment of receipt — that opens an animated-emoji-driven DDoS vector.
|
||||||
|
Image fetch SHOULD be batched and rate-limited.
|
||||||
|
6. A reactor MAY include the same `a`-tagged reaction more than once
|
||||||
|
(e.g. spamming claps). Receivers MUST NOT de-duplicate by content
|
||||||
|
alone; events with different ids are always distinct.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
A clap reaction targeted at a speaker:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7,
|
||||||
|
"pubkey": "ghi...audience",
|
||||||
|
"created_at": 1714003220,
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:abchost:office-hours-2026-04"],
|
||||||
|
["p", "def...co"]
|
||||||
|
],
|
||||||
|
"content": "👏",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A custom-emoji reaction (NIP-30):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 7,
|
||||||
|
"pubkey": "ghi...audience",
|
||||||
|
"created_at": 1714003221,
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:abchost:office-hours-2026-04"],
|
||||||
|
["emoji", "amethyst", "https://example/amethyst.png"]
|
||||||
|
],
|
||||||
|
"content": ":amethyst:",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-06 is independent from EGG-05 (chat). A peer MAY implement either,
|
||||||
|
both, or neither without affecting interop on EGG-01 through EGG-04.
|
||||||
|
|
||||||
|
Future EGGs MAY introduce paid reactions (`kind:9735` zap receipts at the
|
||||||
|
same `a`-tag). Implementers SHOULD be prepared to source the reaction
|
||||||
|
overlay from both `kind:7` and `kind:9735` streams.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# EGG-07: Roles & moderation (`kind:4312`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01, EGG-04`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Hosts and admins manage the room by re-publishing the `kind:30312` event
|
||||||
|
with updated `p`-tag role markers, and (for kicks) by emitting a separate
|
||||||
|
`kind:4312` admin command. Authorization is signature-based: receivers
|
||||||
|
gate inbound commands on the signer's role at the time of receipt.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
### Role markers (in `kind:30312`)
|
||||||
|
|
||||||
|
The 4th element of a `p`-tag, when present, is the role:
|
||||||
|
|
||||||
|
```
|
||||||
|
["p", "<pubkey>", "<relay hint>", "host" | "admin" | "speaker"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Effective roles:
|
||||||
|
|
||||||
|
| value | privileges |
|
||||||
|
|------------|-----------------------------------------------------------|
|
||||||
|
| `host` | full control; one host per room (the event author) |
|
||||||
|
| `admin` | promote / demote / kick; cannot edit room metadata |
|
||||||
|
| `speaker` | may publish audio (EGG-03 `put` claim required) |
|
||||||
|
| (omitted) | listener — no publish rights |
|
||||||
|
|
||||||
|
### Admin command (`kind:4312`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 4312,
|
||||||
|
"pubkey": "<host or admin pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
|
||||||
|
["p", "<target pubkey hex>"],
|
||||||
|
["action", "kick"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`kind:4312` is an *ephemeral* event — receivers MUST NOT persist it past
|
||||||
|
the 60-second validity window defined below.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
### Promote / demote
|
||||||
|
|
||||||
|
1. To promote a listener to speaker, the host or an admin MUST re-publish
|
||||||
|
the `kind:30312` event with the target added (or updated) as
|
||||||
|
`["p", <target>, "<relay>", "speaker"]`. The new event's `created_at`
|
||||||
|
MUST be greater than the previous one.
|
||||||
|
2. To demote, the host or admin MUST re-publish the `kind:30312` event
|
||||||
|
without the role marker (drop the 4th element of the `p`-tag) or
|
||||||
|
remove the `p`-tag entirely.
|
||||||
|
3. Hosts MUST NOT demote themselves. Demoting the host (changing the
|
||||||
|
event author's `p`-tag from `host`) is undefined and MAY be ignored
|
||||||
|
by receivers.
|
||||||
|
4. The `host` role is determined by event authorship, not by `p`-tag
|
||||||
|
marker. The `["p", <author>, _, "host"]` tag is descriptive only.
|
||||||
|
5. Speakers and admins SHOULD NOT delete or edit each other's role
|
||||||
|
markers in their own re-publishes (only the host's re-publish is
|
||||||
|
authoritative, but admins MAY drive promote/demote).
|
||||||
|
|
||||||
|
### Kick
|
||||||
|
|
||||||
|
6. To kick a participant, a host or admin signs and broadcasts a
|
||||||
|
`kind:4312` event with `action="kick"` and a `p`-tag pointing at the
|
||||||
|
target.
|
||||||
|
7. Receivers MUST gate inbound `kind:4312` events:
|
||||||
|
- The signer MUST currently hold `host` or `admin` role on the active
|
||||||
|
`kind:30312` (most recent `created_at` per `(kind, pubkey, d)`).
|
||||||
|
- The event's `created_at` MUST be within the last 60 s.
|
||||||
|
- The `a`-tag MUST match the room the receiver is in.
|
||||||
|
- The `["action", "kick"]` element MUST be present.
|
||||||
|
Events failing any gate MUST be silently discarded.
|
||||||
|
8. The TARGET of a kick MUST disconnect from the audio plane (close the
|
||||||
|
moq-lite session) and tear down the in-room UI.
|
||||||
|
9. The kick command does NOT also demote the target. The host MAY follow
|
||||||
|
up with a `kind:30312` re-publish dropping the target's role tag; the
|
||||||
|
client-side filter then prevents future presence events from the
|
||||||
|
target re-rendering them in the participant grid.
|
||||||
|
10. The relay MUST NOT enforce kicks. Authorization is purely
|
||||||
|
client-side and signature-based.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Host promotes a listener to speaker:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"created_at": 1714003250,
|
||||||
|
"tags": [
|
||||||
|
["d", "office-hours-2026-04"],
|
||||||
|
["room", "Office Hours"],
|
||||||
|
["status", "open"],
|
||||||
|
["service", "https://moq.nostrnests.com"],
|
||||||
|
["endpoint", "https://moq.nostrnests.com"],
|
||||||
|
["p", "abc...host", "wss://relay.example", "host"],
|
||||||
|
["p", "def...co", "wss://relay.example", "speaker"],
|
||||||
|
["p", "ghi...newSpeaker", "wss://relay.example", "speaker"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin kicks a disruptive participant:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 4312,
|
||||||
|
"pubkey": "jkl...admin",
|
||||||
|
"created_at": 1714003280,
|
||||||
|
"tags": [
|
||||||
|
["a", "30312:abchost:office-hours-2026-04"],
|
||||||
|
["p", "mno...troll"],
|
||||||
|
["action", "kick"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
`kind:4312` is intentionally a Nostr event, not a moq-lite control message,
|
||||||
|
so non-broadcasting clients (web, mobile, headless bots) can apply
|
||||||
|
moderation without speaking to the relay.
|
||||||
|
|
||||||
|
Future EGGs MAY introduce additional `action` values (e.g. `"mute"`,
|
||||||
|
`"warn"`). Implementers MUST treat unknown actions as no-ops.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# EGG-08: Scheduling
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A host can announce a room before it opens by publishing a `kind:30312`
|
||||||
|
with `status="planned"` and a `["starts", <unix-seconds>]` tag. Listing
|
||||||
|
clients render these as upcoming-room cards; the audio plane stays cold
|
||||||
|
until the host transitions the room to `open`.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "<host pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["d", "<room d>"],
|
||||||
|
["room", "<room name>"],
|
||||||
|
["status", "planned"],
|
||||||
|
["starts", "<unix seconds>"],
|
||||||
|
["service", "<...>"],
|
||||||
|
["endpoint","<...>"],
|
||||||
|
["p", "<host pubkey>", "<relay hint>", "host"]
|
||||||
|
// ...other EGG-01 tags
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `starts` value is a base-10 ASCII unix timestamp in seconds. Clients
|
||||||
|
that need finer-than-second precision are out of scope; round to the
|
||||||
|
nearest second.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. A planned room MUST carry `status="planned"` AND a `starts` tag with
|
||||||
|
a non-empty numeric value. A planned room without a `starts` value
|
||||||
|
MUST be treated as un-renderable (rooms with unknown start times
|
||||||
|
pollute the listing UI).
|
||||||
|
2. The `starts` value SHOULD be in the future at the time the event is
|
||||||
|
published. Receivers MUST tolerate past values: a planned room whose
|
||||||
|
`starts` is in the past renders as "starts soon" rather than
|
||||||
|
surfacing the past time.
|
||||||
|
3. To start a planned room, the host MUST re-publish the same `(kind, d)`
|
||||||
|
with `status="open"` and a higher `created_at`. The host MAY drop the
|
||||||
|
`starts` tag at this point (no longer informative).
|
||||||
|
4. To cancel a planned room, the host MUST re-publish the same
|
||||||
|
`(kind, d)` with `status="closed"`. The `starts` tag SHOULD be
|
||||||
|
preserved so receivers can label the cancellation
|
||||||
|
("Cancelled — was scheduled for ...").
|
||||||
|
5. Planned rooms MUST NOT mint moq-auth tokens (EGG-02). The auth sidecar
|
||||||
|
SHOULD reject `POST /auth` requests targeting a `(host, room d)` whose
|
||||||
|
most-recent `kind:30312` is `status="planned"`.
|
||||||
|
6. Listing UIs SHOULD sort planned rooms ahead of live ones until 5
|
||||||
|
minutes before `starts`, then promote the room to the same surface as
|
||||||
|
live rooms (the host is unlikely to be more than 5 minutes late).
|
||||||
|
7. A peer MUST NOT use `["expiration", <ts>]` (NIP-40) on a planned
|
||||||
|
`kind:30312`. The implicit 8-hour staleness window from EGG-01 is
|
||||||
|
the only auto-close mechanism.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"created_at": 1714000000,
|
||||||
|
"tags": [
|
||||||
|
["d", "monthly-q-and-a"],
|
||||||
|
["room", "Monthly Q&A"],
|
||||||
|
["summary", "Bring your questions"],
|
||||||
|
["status", "planned"],
|
||||||
|
["starts", "1714172400"],
|
||||||
|
["service", "https://moq.nostrnests.com"],
|
||||||
|
["endpoint", "https://moq.nostrnests.com"],
|
||||||
|
["p", "abc...host", "wss://relay.example", "host"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Receivers without EGG-08 see a planned room as `status="planned"` and
|
||||||
|
SHOULD treat it as un-joinable per EGG-01 rule 6 (status-not-`open`
|
||||||
|
means non-renderable for live UIs). The room becomes joinable as soon
|
||||||
|
as the host re-publishes with `status="open"`.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# EGG-09: User server list (`kind:10112`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: NIP-01`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A user MAY publish a list of nests servers they prefer to host on. Clients
|
||||||
|
read this list to default-fill the "service" and "endpoint" fields when the
|
||||||
|
user opens a new room, and to suggest peer rooms in discovery surfaces.
|
||||||
|
|
||||||
|
This is the audio-rooms equivalent of the user-server-list patterns in
|
||||||
|
Blossom (BUD-03) and Mostr.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
`kind:10112` is a NIP-01 *replaceable* event with one entry per server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 10112,
|
||||||
|
"pubkey": "<user pubkey hex>",
|
||||||
|
"tags": [
|
||||||
|
["server", "<https URL — moq-auth/moq-relay base>"],
|
||||||
|
["server", "<https URL>"],
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `["server", url]` is one entry. The relative ordering MUST be
|
||||||
|
preserved by receivers — earlier entries are higher priority.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Each `server` value MUST be a fully-qualified URL beginning with
|
||||||
|
`https://`. Receivers MUST reject (drop) entries that are not
|
||||||
|
well-formed HTTPS URLs.
|
||||||
|
2. Receivers MUST de-duplicate entries by exact-string match after
|
||||||
|
trimming a single trailing `/`. Order of remaining entries MUST be
|
||||||
|
preserved (the FIRST occurrence wins).
|
||||||
|
3. When the user opens the create-room sheet, the client SHOULD pre-fill
|
||||||
|
the EGG-01 `service` and `endpoint` fields from the FIRST entry in
|
||||||
|
the list (a single nests deployment serves both via the same base
|
||||||
|
URL today).
|
||||||
|
4. Users MAY enumerate up to 64 servers. Receivers MUST tolerate longer
|
||||||
|
lists by truncating to the first 64.
|
||||||
|
5. Hosts who list a server in their `kind:10112` are not declaring an
|
||||||
|
alliance — they are merely advertising "I will probably create rooms
|
||||||
|
here". Receivers MUST NOT use the list as a moderation signal.
|
||||||
|
6. The list is purely a defaults / discovery hint. A `kind:30312` event's
|
||||||
|
own `service` / `endpoint` tags are authoritative for that specific
|
||||||
|
room and override the user list at join time.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 10112,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"created_at": 1714003000,
|
||||||
|
"tags": [
|
||||||
|
["server", "https://moq.nostrnests.com"],
|
||||||
|
["server", "https://moq.example.org"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"id": "...",
|
||||||
|
"sig": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
A peer that does not implement EGG-09 simply does not pre-fill server
|
||||||
|
fields and does not surface "rooms hosted by this user are usually on…"
|
||||||
|
hints in profile screens. All other interop is unaffected.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# EGG-10: Theming
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: decorative`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A host MAY skin their room with custom colors, font, and a background
|
||||||
|
image. Theme tags are added to the existing `kind:30312` event; clients
|
||||||
|
without theme support ignore them and render with their default theme.
|
||||||
|
|
||||||
|
This EGG is decorative: theming MUST NOT affect protocol semantics
|
||||||
|
(joining, audio, presence, chat). A non-themed renderer MUST NOT show
|
||||||
|
fewer features than a themed one.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
Three optional tags on `kind:30312`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["c", "<hex color>", "background" | "text" | "primary"]
|
||||||
|
["f", "<font family>", "<optional font URL>"]
|
||||||
|
["bg", "<background image URL>", "cover" | "tile"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color (`c`)
|
||||||
|
|
||||||
|
- The hex value is six hex digits (`#RRGGBB`); a leading `#` is OPTIONAL.
|
||||||
|
Three-digit shorthand (`#abc`) is NOT supported.
|
||||||
|
- The third element selects the role: `background`, `text`, or `primary`.
|
||||||
|
- A room MAY include up to one `c` tag per role. Extras MUST be ignored
|
||||||
|
by receivers (palette fallbacks are reserved for a future EGG).
|
||||||
|
|
||||||
|
### Font (`f`)
|
||||||
|
|
||||||
|
- The family name is a free-form string. Receivers SHOULD match the
|
||||||
|
four CSS-style generic names case-insensitively: `sans-serif`,
|
||||||
|
`serif`, `monospace`, `cursive`. Other names fall through to the
|
||||||
|
platform default unless the renderer also supports the URL.
|
||||||
|
- The URL element is OPTIONAL. When present, it MUST be a fully-qualified
|
||||||
|
HTTPS URL pointing at a single-style font file (TTF/OTF/WOFF2). HTTP
|
||||||
|
URLs MUST be rejected (mixed-content + integrity concerns).
|
||||||
|
|
||||||
|
### Background (`bg`)
|
||||||
|
|
||||||
|
- The URL is a fully-qualified HTTPS URL pointing at an image (PNG/JPEG/
|
||||||
|
WEBP).
|
||||||
|
- The mode is `cover` (scale to fill, crop overflow) or `tile` (repeat
|
||||||
|
on both axes). Unknown modes MUST fall back to `cover`.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Theme tags are PARSED ONLY by clients with rendering support. Clients
|
||||||
|
without that support MUST ignore them silently.
|
||||||
|
2. Color values MUST be parsed strictly: `^#?[0-9a-fA-F]{6}$`. Anything
|
||||||
|
else MUST result in the role falling back to the platform default,
|
||||||
|
not in a render error.
|
||||||
|
3. The `text` color is the foreground for room name, summary, and chat
|
||||||
|
bodies. The `background` color paints behind everything except the
|
||||||
|
`bg` image (image overlays the color). The `primary` color drives
|
||||||
|
accent surfaces (active-speaker ring, send button).
|
||||||
|
4. Font URL fetching MUST be opt-in to the renderer's image / asset
|
||||||
|
pipeline (Tor or proxy if so configured). Renderers MUST cache the
|
||||||
|
fetched font on disk keyed by URL hash; the same URL MUST NOT be
|
||||||
|
re-fetched in the same session.
|
||||||
|
5. Background image fetching MUST go through the same channel as
|
||||||
|
inline images (typically the renderer's image cache).
|
||||||
|
6. Themed renderers MUST tolerate a TIME-OF-FETCH gap: the room screen
|
||||||
|
opens with platform defaults, font / background swap in once
|
||||||
|
loaded. There MUST be no flash-of-blank-screen.
|
||||||
|
7. A room theme MUST NOT alter element positions, sizes, or behavior.
|
||||||
|
This EGG controls colors, font family, and a background image — no
|
||||||
|
layout primitives.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"tags": [
|
||||||
|
["d", "purple-room"],
|
||||||
|
["room", "Purple"],
|
||||||
|
["status", "open"],
|
||||||
|
["service", "..."],
|
||||||
|
["endpoint", "..."],
|
||||||
|
["p", "abc...host", "wss://relay", "host"],
|
||||||
|
["c", "#1a0033", "background"],
|
||||||
|
["c", "#FFFFFF", "text"],
|
||||||
|
["c", "#a020f0", "primary"],
|
||||||
|
["f", "Inter", "https://fonts.example/inter.woff2"],
|
||||||
|
["bg", "https://example.com/stars.png", "tile"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"...": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
A non-themed receiver renders the room without any of the cosmetic
|
||||||
|
overrides. A themed receiver that fails to fetch one of the assets
|
||||||
|
(font / background) MUST render the rest of the theme and the room is
|
||||||
|
still fully functional.
|
||||||
|
|
||||||
|
Future EGGs MAY introduce per-user profile themes (e.g. `kind:16767`)
|
||||||
|
and shared theme references (e.g. `kind:36767` Ditto themes). Those
|
||||||
|
specs MUST be additive: a theme defined on the room itself overrides
|
||||||
|
any per-user or shared theme.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# EGG-11: Recording
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-01`
|
||||||
|
`category: decorative`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
When a host captures a room out-of-band and wants to make the recording
|
||||||
|
available to audience members who missed the live session, they re-publish
|
||||||
|
the closed `kind:30312` with a `["recording", url]` tag.
|
||||||
|
|
||||||
|
The recording itself is not delivered through the audio plane (EGG-03) —
|
||||||
|
it is a static asset on the open web. Clients hand the URL to the system
|
||||||
|
media player rather than embedding playback inline.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
A `["recording", url]` tag added to a `kind:30312` event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["recording", "<https URL of the recording>"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The URL MUST point at a publicly-fetchable audio file (Opus, MP3, AAC, M4A,
|
||||||
|
or any format the local OS / app ecosystem supports for `audio/*` MIME
|
||||||
|
types).
|
||||||
|
|
||||||
|
A `kind:30312` carrying a `recording` tag MUST also carry `status="closed"`.
|
||||||
|
A live or planned room with a recording tag is non-conformant; receivers
|
||||||
|
SHOULD ignore the recording tag in that case.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. The host MUST NOT publish a `recording` tag until the recording is
|
||||||
|
uploaded and reachable. A recording tag pointing at a 404 erodes
|
||||||
|
trust; receivers SHOULD silently hide the listen-back UI when the
|
||||||
|
URL fails to resolve.
|
||||||
|
2. The URL MUST be `https://`. `http://` URLs MUST be rejected.
|
||||||
|
3. The host SHOULD include a `Content-Type` header on the asset's HTTP
|
||||||
|
response so the system media player can pick the right handler.
|
||||||
|
4. Receivers SHOULD render the listen-back affordance as a single button
|
||||||
|
("Listen to recording") that hands the URL to the platform's media
|
||||||
|
intent (Android: `ACTION_VIEW`; iOS: `UIApplication.open`; web:
|
||||||
|
`<a target="_blank">`). Receivers MUST NOT auto-play.
|
||||||
|
5. Receivers MUST gracefully tolerate the absence of any registered
|
||||||
|
media handler — show the user a "no app installed to play this"
|
||||||
|
toast rather than crash or no-op silently.
|
||||||
|
6. Clients MUST NOT subscribe to the audio plane (EGG-03) for a closed
|
||||||
|
room. The recording is the only audio path post-close.
|
||||||
|
7. Hosts MAY publish multiple recording tags (e.g. one MP3, one Opus).
|
||||||
|
Receivers SHOULD prefer the FIRST entry and treat extras as
|
||||||
|
alternatives.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 30312,
|
||||||
|
"pubkey": "abc...host",
|
||||||
|
"created_at": 1714010000,
|
||||||
|
"tags": [
|
||||||
|
["d", "office-hours-2026-04"],
|
||||||
|
["room", "Office Hours"],
|
||||||
|
["status", "closed"],
|
||||||
|
["service", "https://moq.nostrnests.com"],
|
||||||
|
["endpoint", "https://moq.nostrnests.com"],
|
||||||
|
["p", "abc...host", "wss://relay", "host"],
|
||||||
|
["recording", "https://recordings.example.com/office-hours-2026-04.opus"]
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"...": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Receivers without EGG-11 see a closed room without a listen-back button.
|
||||||
|
A receiver with EGG-11 but without a registered media app for the URL's
|
||||||
|
MIME type falls back to the toast described in rule 5.
|
||||||
|
|
||||||
|
Recording is not part of the moq-lite spec; it is a Nostr-side
|
||||||
|
augmentation. A nests deployment that does not capture rooms continues
|
||||||
|
to interop without changes.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# EGG-12: Catalog track (`catalog.json`)
|
||||||
|
|
||||||
|
`status: draft`
|
||||||
|
`requires: EGG-03`
|
||||||
|
`category: optional`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A speaker MAY publish a sibling `catalog.json` track on the same moq-lite
|
||||||
|
broadcast as their `audio/data` track. The catalog carries codec
|
||||||
|
metadata: format, sample rate, channel count, bitrate, etc. It exists so
|
||||||
|
clients can render a "speaker is broadcasting Opus 48 kHz mono" tooltip
|
||||||
|
without parsing audio frames, and so future codec migrations can be
|
||||||
|
negotiated client-side.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
### Track convention
|
||||||
|
|
||||||
|
A speaker MUST publish the catalog on the SAME broadcast as their audio
|
||||||
|
track:
|
||||||
|
|
||||||
|
```
|
||||||
|
SUBSCRIBE broadcast=<speaker pubkey hex> track="catalog.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Payload
|
||||||
|
|
||||||
|
Each `Group` carries a single JSON document encoded as UTF-8. The
|
||||||
|
publisher emits a fresh group whenever the catalog changes (e.g. codec
|
||||||
|
swap on mute / unmute). Receivers SHOULD read only the most recent
|
||||||
|
group.
|
||||||
|
|
||||||
|
Document shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"audio": [
|
||||||
|
{
|
||||||
|
"track": "audio/data",
|
||||||
|
"codec": "opus",
|
||||||
|
"sample_rate": 48000,
|
||||||
|
"channel_count": 1,
|
||||||
|
"bitrate": 32000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Field reference:
|
||||||
|
|
||||||
|
| field | type | required | meaning |
|
||||||
|
|-----------------|--------|----------|--------------------------------------|
|
||||||
|
| `version` | int | required | Schema version. Currently `1`. |
|
||||||
|
| `audio` | array | required | One entry per audio track. May be empty. |
|
||||||
|
| `audio[].track` | string | required | Track name; matches the moq-lite track this catalog describes (typically `"audio/data"`). |
|
||||||
|
| `audio[].codec` | string | optional | Lower-case codec id (`opus`, `aac`, …). |
|
||||||
|
| `audio[].sample_rate` | int | optional | Sample rate in Hz. |
|
||||||
|
| `audio[].channel_count` | int | optional | 1 = mono, 2 = stereo. |
|
||||||
|
| `audio[].bitrate` | int | optional | Target bitrate in bits per second. |
|
||||||
|
|
||||||
|
Unknown fields MUST be tolerated: parsers MUST ignore keys they do not
|
||||||
|
recognise rather than rejecting the document. New keys are added without
|
||||||
|
incrementing `version` unless they change the meaning of an existing key.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Publishing a catalog is OPTIONAL. A speaker that omits the
|
||||||
|
`catalog.json` track MUST still be playable per EGG-03 (which already
|
||||||
|
pins the audio parameters).
|
||||||
|
2. A subscriber MUST tolerate the absence of a catalog. A `Subscribe`
|
||||||
|
response of `Drop`/error on the `catalog.json` track is benign — the
|
||||||
|
subscriber falls back to the EGG-03 default parameters.
|
||||||
|
3. A subscriber MUST tolerate malformed JSON, missing `version`, and
|
||||||
|
unknown fields. Failure to parse means "no catalog metadata" — render
|
||||||
|
the default tooltip.
|
||||||
|
4. A publisher MUST NOT use the catalog track to deliver authoritative
|
||||||
|
stream parameters that contradict EGG-03. The catalog is INFORMATIVE,
|
||||||
|
not normative: a listener MUST be able to decode `audio/data` without
|
||||||
|
reading the catalog.
|
||||||
|
5. Subscribers SHOULD limit how often they re-render the catalog tooltip:
|
||||||
|
at most once per second, even if catalogs are published more
|
||||||
|
frequently.
|
||||||
|
6. Hosts MAY use the catalog to detect non-conformant publishers (e.g. a
|
||||||
|
broadcaster claiming `aac` when EGG-03 mandates `opus`) for
|
||||||
|
moderation purposes. The actual moderation flow is out of scope for
|
||||||
|
this EGG.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
A speaker comes onstage with default Opus parameters:
|
||||||
|
|
||||||
|
```
|
||||||
|
SUBSCRIBE broadcast="speakerA" track="catalog.json"
|
||||||
|
← GROUP sequence=0
|
||||||
|
← FRAME {"version":1,"audio":[{"track":"audio/data","codec":"opus","sample_rate":48000,"channel_count":1,"bitrate":32000}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same speaker bumps to 64 kbit/s:
|
||||||
|
|
||||||
|
```
|
||||||
|
← GROUP sequence=1
|
||||||
|
← FRAME {"version":1,"audio":[{"track":"audio/data","codec":"opus","sample_rate":48000,"channel_count":1,"bitrate":64000}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
EGG-12 is purely additive on top of EGG-03. A receiver that does not
|
||||||
|
support EGG-12 MUST still subscribe to `audio/data` directly (EGG-03)
|
||||||
|
and decode at the parameters EGG-03 mandates.
|
||||||
|
|
||||||
|
A future EGG MAY widen the catalog schema to describe multiple audio
|
||||||
|
quality tiers, video tracks, or speaker-side hints (e.g. push-to-talk
|
||||||
|
state). Those additions MUST keep `version: 1` working for existing
|
||||||
|
parsers; breaking changes ship as `version: 2` with a separate EGG.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# EGGs — Extensible Gossip Guidelines
|
||||||
|
|
||||||
|
Wire-protocol specs for nostrnests-style audio rooms. Each EGG defines one
|
||||||
|
self-contained capability that a client or relay can implement. Two compliant
|
||||||
|
peers that implement the same set of EGGs round-trip without further
|
||||||
|
coordination.
|
||||||
|
|
||||||
|
Specs intentionally mirror the [Nostr NIP](https://github.com/nostr-protocol/nips)
|
||||||
|
and [Blossom BUD](https://github.com/hzrd149/blossom) formats: ASCII Markdown,
|
||||||
|
RFC-2119 keywords (MUST / SHOULD / MAY), one capability per file.
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| # | File | Topic | Category |
|
||||||
|
|----|----------------|-------------------------------------------|------------|
|
||||||
|
| 01 | [EGG-01](./EGG-01.md) | Room event (`kind:30312`) | required |
|
||||||
|
| 02 | [EGG-02](./EGG-02.md) | Auth & WebTransport handshake | required |
|
||||||
|
| 03 | [EGG-03](./EGG-03.md) | Audio plane (moq-lite) | required |
|
||||||
|
| 04 | [EGG-04](./EGG-04.md) | Presence (`kind:10312`) | required |
|
||||||
|
| 05 | [EGG-05](./EGG-05.md) | In-room chat (`kind:1311`) | optional |
|
||||||
|
| 06 | [EGG-06](./EGG-06.md) | Reactions (`kind:7`) | optional |
|
||||||
|
| 07 | [EGG-07](./EGG-07.md) | Roles & moderation (`kind:4312`) | optional |
|
||||||
|
| 08 | [EGG-08](./EGG-08.md) | Scheduling (`status=planned`) | optional |
|
||||||
|
| 09 | [EGG-09](./EGG-09.md) | User server list (`kind:10112`) | optional |
|
||||||
|
| 10 | [EGG-10](./EGG-10.md) | Theming (`c`/`f`/`bg`) | decorative |
|
||||||
|
| 11 | [EGG-11](./EGG-11.md) | Recording (`recording` tag) | decorative |
|
||||||
|
| 12 | [EGG-12](./EGG-12.md) | Catalog track (`catalog.json`) | optional |
|
||||||
|
|
||||||
|
## Conformance levels
|
||||||
|
|
||||||
|
A peer claims **Listener compliance** when it implements EGG-01, EGG-02, EGG-03
|
||||||
|
and at least the read side of EGG-04.
|
||||||
|
|
||||||
|
A peer claims **Speaker compliance** when it implements all of the above plus
|
||||||
|
the publish side of EGG-03 and the write side of EGG-04.
|
||||||
|
|
||||||
|
A peer claims **Host compliance** when it implements Speaker compliance plus
|
||||||
|
EGG-07 and the write side of EGG-08 (if scheduled rooms are exposed in its UI).
|
||||||
|
|
||||||
|
EGG-05 through EGG-12 are independently optional. Lacking any of them MUST NOT
|
||||||
|
break interop on the EGGs a peer does implement.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Each spec carries a `status` line at the top: `draft` (subject to change),
|
||||||
|
`accepted` (frozen except for clarifications), or `replaced-by: EGG-XX`
|
||||||
|
(superseded). Breaking changes ship as a new EGG number; existing numbers are
|
||||||
|
never re-purposed.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
EGG = Extensible Gossip Guideline. The acronym is intentional: nostrnests
|
||||||
|
serves nests; nests hold eggs.
|
||||||
|
|
||||||
|
## Filing changes
|
||||||
|
|
||||||
|
Edit a single EGG per pull request. Include a wire-format example showing the
|
||||||
|
delta. If a change crosses two specs, file two PRs and reference each from the
|
||||||
|
other.
|
||||||
Reference in New Issue
Block a user