diff --git a/nestsClient/specs/EGG-01.md b/nestsClient/specs/EGG-01.md new file mode 100644 index 000000000..f965845f4 --- /dev/null +++ b/nestsClient/specs/EGG-01.md @@ -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": "", + "tags": [ + ["d", ""], + ["room", ""], + ["summary", ""], + ["image", ""], + ["status", "open" | "private" | "closed" | "planned"], + ["service", ""], + ["endpoint", ""], + ["relays", "", "", ...], + ["p", "", "", "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. diff --git a/nestsClient/specs/EGG-02.md b/nestsClient/specs/EGG-02.md new file mode 100644 index 000000000..15db48b7f --- /dev/null +++ b/nestsClient/specs/EGG-02.md @@ -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 /auth +Authorization: Nostr +Content-Type: application/json + +{ "namespace": "nests/::", + "publish": } +``` + +`` 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": "" } +``` + +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: `[]`. | +| `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 = +:path = /?jwt= +``` + +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. diff --git a/nestsClient/specs/EGG-03.md b/nestsClient/specs/EGG-03.md new file mode 100644 index 000000000..7f4e88a4f --- /dev/null +++ b/nestsClient/specs/EGG-03.md @@ -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 = +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 = +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 ``. + +## Example + +A two-speaker room: + +``` +A's session: ANNOUNCE prefix="" suffix="speakerA" status=Active + GROUP subscribe-id=…, sequence=0 + FRAME + FRAME + … + +Listener's session: + SUBSCRIBE broadcast="speakerA" track="audio/data" + ← GROUP sequence=0 + ← FRAME + ← … + SUBSCRIBE broadcast="speakerB" track="audio/data" + ← GROUP sequence=0 + ← FRAME + ← … +``` + +## 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`. diff --git a/nestsClient/specs/EGG-04.md b/nestsClient/specs/EGG-04.md new file mode 100644 index 000000000..85abd6c2c --- /dev/null +++ b/nestsClient/specs/EGG-04.md @@ -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": "", + "tags": [ + ["a", "30312::", ""], + ["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. diff --git a/nestsClient/specs/EGG-05.md b/nestsClient/specs/EGG-05.md new file mode 100644 index 000000000..c7b5cc63a --- /dev/null +++ b/nestsClient/specs/EGG-05.md @@ -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": "", + "tags": [ + ["a", "30312::", ""] + ], + "content": "", + ... +} +``` + +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. diff --git a/nestsClient/specs/EGG-06.md b/nestsClient/specs/EGG-06.md new file mode 100644 index 000000000..ccb8e1403 --- /dev/null +++ b/nestsClient/specs/EGG-06.md @@ -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. diff --git a/nestsClient/specs/EGG-07.md b/nestsClient/specs/EGG-07.md new file mode 100644 index 000000000..eb3700dc8 --- /dev/null +++ b/nestsClient/specs/EGG-07.md @@ -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. diff --git a/nestsClient/specs/EGG-08.md b/nestsClient/specs/EGG-08.md new file mode 100644 index 000000000..2270affb0 --- /dev/null +++ b/nestsClient/specs/EGG-08.md @@ -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"`. diff --git a/nestsClient/specs/EGG-09.md b/nestsClient/specs/EGG-09.md new file mode 100644 index 000000000..8bdb5f478 --- /dev/null +++ b/nestsClient/specs/EGG-09.md @@ -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. diff --git a/nestsClient/specs/EGG-10.md b/nestsClient/specs/EGG-10.md new file mode 100644 index 000000000..c80a7ed16 --- /dev/null +++ b/nestsClient/specs/EGG-10.md @@ -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. diff --git a/nestsClient/specs/EGG-11.md b/nestsClient/specs/EGG-11.md new file mode 100644 index 000000000..0ca302ee9 --- /dev/null +++ b/nestsClient/specs/EGG-11.md @@ -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. diff --git a/nestsClient/specs/EGG-12.md b/nestsClient/specs/EGG-12.md new file mode 100644 index 000000000..e368700cf --- /dev/null +++ b/nestsClient/specs/EGG-12.md @@ -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. diff --git a/nestsClient/specs/README.md b/nestsClient/specs/README.md new file mode 100644 index 000000000..0e3abadf9 --- /dev/null +++ b/nestsClient/specs/README.md @@ -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.