From bd12d5ef72ded400b5b72f2ff3922e087816f12c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 12:31:19 +0000 Subject: [PATCH] docs(nestsClient/specs): close interop gaps surfaced in spec review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edits across all 13 EGGs to remove implementer guesswork. After this an implementer can build a listener and speaker without reading our source. README: - Conventions section: hex casing rule (lowercase, 64 chars, no 0x), NIP-01 foundations, `a`-tag form, created_at tie-break, JSON / time. - Joining sequence: numbered end-to-end walkthrough from `kind:30312` to first audio frame, referencing each EGG. EGG-01 (room event): - `relays` tag is one tag with multiple values (not multi-tag). - `service` URL trailing-slash normalization. - `private` status: gate is implementation-defined, not "render as open". - `d`-tag charset locked to [A-Za-z0-9._-] so it interpolates safely into the moq-auth namespace. - Relay-discovery rule: publish to `relays` tag ∪ NIP-65 outbox. - Tie-break on identical created_at via smallest event id. EGG-02 (auth): - JWT signing pinned: ES256 over P-256, JWKS at /.well-known/jwks.json, 5-minute relay cache. - NIP-98 tags pinned: u / method / payload, base64 RFC 4648 standard (not base64url). - Error taxonomy: full HTTP status + `error` slug table for /auth, plus WebTransport CONNECT 200/401/403/404 table. EGG-03 (audio): - Pin moq-lite Lite-03 to kixelated/moq-rs `rs/moq-lite/src/lite/`. - One Opus packet per moq Frame (no container, no timestamp). - Pubkey hex casing reaffirmed at the suffix / broadcast slots. - AnnouncePlease prefix="" for speaker discovery. - Mute = stop publishing (not silence frames, not Announce Ended). - Mid-stream join: discard pre-skip per RFC 7845. EGG-04 (presence): - Heartbeat jitter ±5 s required (anti-thundering-herd). - "0"/"1" are strings, not booleans. - Single-room rule via replaceable-event semantics. EGG-05 (chat): 8 KB suggested, 64 KB hard cap, 3 msg/s render rate. EGG-06 (reactions): 30s window measured against created_at, drop-on-arrival if already stale. EGG-07 (moderation): replay protection — dedupe kicks by id within 120 s. EGG-08 (scheduling): planned rooms MUST 403 at the auth sidecar. EGG-10 (theming): bg image caps (1 MB / 4096 px soft, 8 MB / 8192 px hard). Deferred (per review): EGG-00 (Conventions as a standalone spec), EGG-13 (capability advertisement), EGG-14 (discovery), test vectors corpus. The "Conventions" section in README covers EGG-00's most urgent content inline. https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b --- nestsClient/specs/EGG-01.md | 36 ++++++++++-- nestsClient/specs/EGG-02.md | 108 ++++++++++++++++++++++++++++++++++-- nestsClient/specs/EGG-03.md | 58 ++++++++++++++++--- nestsClient/specs/EGG-04.md | 15 ++++- nestsClient/specs/EGG-05.md | 6 ++ nestsClient/specs/EGG-06.md | 8 ++- nestsClient/specs/EGG-07.md | 3 + nestsClient/specs/EGG-08.md | 7 ++- nestsClient/specs/EGG-10.md | 4 ++ nestsClient/specs/README.md | 102 ++++++++++++++++++++++++++++++++++ 10 files changed, 325 insertions(+), 22 deletions(-) diff --git a/nestsClient/specs/EGG-01.md b/nestsClient/specs/EGG-01.md index f965845f4..1cb5b4f5e 100644 --- a/nestsClient/specs/EGG-01.md +++ b/nestsClient/specs/EGG-01.md @@ -26,7 +26,7 @@ Every other EGG layers on top of this event. ["status", "open" | "private" | "closed" | "planned"], ["service", ""], ["endpoint", ""], - ["relays", "", "", ...], + ["relays", "", "", ...], // ONE tag, multiple values ["p", "", "", "host" | "admin" | "speaker"], ... ], @@ -37,6 +37,12 @@ Every other EGG layers on top of this event. The `d` tag is the room id; `(pubkey, kind, d)` is the addressable identity. +The `relays` tag is encoded as a SINGLE tag whose first element is `"relays"` +and whose remaining elements are wss URLs. Implementers MUST NOT emit one +`["relays", url]` tag per relay; receivers MUST treat such input as a +malformed event and MAY repair it by concatenating the values, but +publishers that emit it are non-conformant. + ## Behavior 1. Hosts MUST emit exactly one `kind:30312` event per room. Updating the room @@ -46,15 +52,21 @@ The `d` tag is the room id; `(pubkey, kind, d)` is the addressable identity. 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). + not include the `/auth` suffix). Receivers MUST normalize the value by + stripping a single trailing `/` before constructing sub-paths. 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`). + - `open` — room is live, anyone can join. The auth sidecar MUST mint a + listener token to any well-formed NIP-98 request. + - `private` — room is live, but the auth sidecar applies an out-of-band + allowlist. The allowlist mechanism is implementation-defined; this + spec only mandates that a non-allowlisted requester receives `403` + (see EGG-02 error taxonomy). Clients without a path to acquire access + MUST render `private` rooms as un-joinable rather than attempt to + connect blind. - `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 @@ -63,6 +75,20 @@ The `d` tag is the room id; `(pubkey, kind, d)` is the addressable identity. 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. +9. The `d` tag MUST contain only characters from `[A-Za-z0-9._-]`. Colons, + slashes, whitespace, and percent-encoded sequences are forbidden because + the `d` is interpolated unescaped into the moq-auth namespace + `nests/::` (EGG-02). Receivers MUST reject + events whose `d` violates this charset. +10. **Relay discovery.** Hosts SHOULD publish the `kind:30312` event to (a) + every relay listed in the event's own `relays` tag, AND (b) their + NIP-65 (`kind:10002`) write relays. Receivers SHOULD subscribe to the + same union to track room metadata changes (rename, status flip, + role grants). +11. **Tie-break on identical `created_at`.** When two `kind:30312` events + share `(pubkey, d)` AND `created_at`, receivers MUST keep the event + with the lexicographically SMALLEST `id` and discard the other (per + NIP-01 replaceable-event tie-break). ## Example diff --git a/nestsClient/specs/EGG-02.md b/nestsClient/specs/EGG-02.md index 15db48b7f..b8e5632ac 100644 --- a/nestsClient/specs/EGG-02.md +++ b/nestsClient/specs/EGG-02.md @@ -20,8 +20,8 @@ sidecar; the `endpoint` tag is the relay. ``` POST /auth -Authorization: Nostr -Content-Type: application/json +Authorization: Nostr +Content-Type: application/json; charset=utf-8 { "namespace": "nests/::", "publish": } @@ -29,8 +29,41 @@ Content-Type: application/json `` 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. +The `` URL is the EGG-01 `service` tag value with any trailing `/` +stripped, then `/auth` appended literally. + +The body is a single-line UTF-8 JSON object — the server hashes the **exact +bytes sent** to compare against NIP-98's `payload` tag, so producers MUST +NOT pretty-print or re-order keys after signing. + +The `` is the host's pubkey, lowercase hex. The `` +is the EGG-01 `d` tag (which by EGG-01 rule 9 cannot contain `:`, so the +namespace is unambiguous without escaping). + +#### NIP-98 event shape + +The signed kind 27235 event MUST carry exactly these tags (NIP-98 §1): + +```json +{ + "kind": 27235, + "pubkey": "", + "created_at": , + "tags": [ + ["u", "/auth"], // exact request URL, scheme included + ["method", "POST"], + ["payload", ""] + ], + "content": "", + "id": "<...>", + "sig": "<...>" +} +``` + +The event JSON is then serialized (NIP-01 canonical form), encoded as +**RFC 4648 standard Base64** (NOT base64url; pad with `=`), prefixed with +`Nostr ` (literal, including the trailing space), and placed in the +`Authorization` header. ### Step 2 — token response @@ -51,6 +84,36 @@ The JWT carries (at minimum): | `iat` | Unix seconds; issuance. | | `exp` | Unix seconds; expiry. MUST be `iat + 600` for nests today. | +#### JWT signing + +The JWT MUST be signed with `alg: "ES256"` (ECDSA on the NIST P-256 curve, +RFC 7518 §3.4). The auth sidecar MUST publish its public verification keys +as a JWKS at: + +``` +GET /.well-known/jwks.json +``` + +The response is an `application/json` body shaped per RFC 7517: + +```json +{ + "keys": [ + { + "kty": "EC", "crv": "P-256", "alg": "ES256", + "use": "sig", "kid": "", + "x": "", "y": "" + } + ] +} +``` + +The relay (EGG-03 endpoint) MUST verify inbound JWTs against this JWKS. +Relays SHOULD cache the JWKS for at most 5 minutes so a key rotation +propagates without requiring a relay restart. A relay that cannot reach +the JWKS endpoint MUST refuse new sessions rather than fall through to +"trust the unverified token". + ### Step 3 — WebTransport CONNECT ``` @@ -87,6 +150,43 @@ header is used at this step. 7. A peer MUST NOT log the JWT or include it in error reports. The token is a bearer credential. +## Error taxonomy + +The auth sidecar MUST use the following HTTP status codes for `POST /auth`. +Bodies are `application/json` and follow the shape +`{ "error": "", "reason": "" }`. Receivers +MAY ignore `reason` but MUST surface `error` to user-facing error toasts. + +| status | `error` slug | when | +|--------|----------------------|---------------------------------------------------------------------| +| 200 | — | success; body is `{ "token": "" }` | +| 400 | `bad_request` | malformed JSON body, missing `namespace`, unknown content-type | +| 400 | `bad_namespace` | namespace does not match `nests/::` | +| 401 | `bad_nip98` | Authorization header missing, malformed, or `id`/`sig` invalid | +| 401 | `wrong_url` | NIP-98 `u` tag does not match the actual request URL | +| 401 | `wrong_method` | NIP-98 `method` tag is not `POST` | +| 401 | `wrong_payload` | NIP-98 `payload` tag does not match sha256 of the body bytes | +| 401 | `stale` | NIP-98 `created_at` outside the ±60 s tolerance | +| 403 | `room_closed` | room status is `closed` (EGG-01) or `planned` (EGG-08) | +| 403 | `not_invited` | room status is `private` and requester is not on the allowlist | +| 403 | `publish_forbidden` | `publish: true` requested but caller is not a speaker per EGG-07 | +| 410 | `unknown_room` | no `kind:30312` known to the sidecar for `(host, d)` | +| 429 | `rate_limited` | per-pubkey or per-IP rate limit; `Retry-After` header SHOULD be set | +| 5xx | `internal` | sidecar internal error | + +Receivers MUST treat unknown 4xx slugs as "fatal, do not retry" and +unknown 5xx slugs as "transient, retry with exponential backoff". + +The relay (EGG-03 endpoint) signals authorization failures through +WebTransport CONNECT response codes, NOT through the auth-sidecar table: + +| WT status | meaning | +|-----------|------------------------------------------------------------------------| +| 200 | session established | +| 401 | JWT signature invalid, expired, or fails the JWKS check | +| 403 | JWT `root` does not match the path namespace, or `put` claim missing for a publishing peer | +| 404 | path namespace unknown to the relay | + ## Example ``` diff --git a/nestsClient/specs/EGG-03.md b/nestsClient/specs/EGG-03.md index 7f4e88a4f..5f607354f 100644 --- a/nestsClient/specs/EGG-03.md +++ b/nestsClient/specs/EGG-03.md @@ -6,16 +6,36 @@ ## 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. +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. +### Normative reference for moq-lite Lite-03 + +The wire formats for `AnnouncePlease`, `Announce`, `Subscribe`, `Group` +header, and `Frame` are defined by the Rust reference implementation at: + +``` +https://github.com/kixelated/moq-rs + rs/moq-lite/src/lite/{announce,subscribe,group,setup,session}.rs +``` + +Implementers MUST track Lite-03 (NOT the IETF moq-transport draft, which +is incompatible). Cross-version interop tests MUST pin a specific +`moq-rs` commit hash for reproducibility. + +All path / broadcast / track strings on the moq-lite wire are +length-prefixed UTF-8. All integers are RFC 9000 §16 varints unless the +upstream codec says otherwise. Path components on this wire MUST contain +only the characters allowed in a URL path segment per RFC 3986; relays +typically reject `/` inside a single path component. + ## Wire format ### Speaker → relay (publish) @@ -24,26 +44,38 @@ A speaker MUST send a moq-lite `Announce` with: ``` prefix = "" (empty — relative to the JWT's `root` namespace) -suffix = -status = Active +suffix = (lowercase, 64 chars, see Conventions in README) +status = Active (status byte 0x01 per Lite-03) +hops = 0 ``` 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. +Each moq-lite `Frame` carries **exactly one Opus packet** (one 20 ms frame). +Producers MUST NOT pack multiple Opus packets into a single Frame, MUST NOT +prefix the payload with an Ogg page or any container, and MUST NOT carry an +explicit timestamp inside the Frame — the receive order plus the fixed 20 ms +cadence is the timing contract. + ### Listener → relay (subscribe) A listener MUST send a moq-lite `Subscribe` with: ``` -broadcast = +broadcast = (lowercase, 64 chars) track = "audio/data" priority = 128 (recommended) ordered = true maxLatency = 0 (unlimited) ``` +To learn which broadcasts exist on this session, a listener sends a +moq-lite `AnnouncePlease` with `prefix=""` once after the moq-lite Setup +handshake completes. The relay then streams `Announce` frames for every +active speaker; the listener subscribes to each per the form above. + The relay forwards each subsequent `Group` and its `Frame`s to the listener in subscribe-id order. @@ -78,6 +110,18 @@ A new `Group` MUST be opened on every Opus reset (e.g. mute/unmute). 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 ``. +8. **Mute behavior.** Muting MUST be implemented as "stop publishing" — the + speaker closes their currently-open Group stream and does NOT open a + new one until unmute. Speakers MUST NOT push silence frames to fake + continuity, and MUST NOT send `Announce status=Ended` on mute (mute is + a transient state; ending the broadcast is reserved for leaving the + stage). The presence event's `muted` flag (EGG-04) communicates the + intent to listeners that lack the audio plane. +9. **Joining mid-stream.** Listeners MUST tolerate joining at any point + inside a Group; the first received Frame is independently decodable + (Opus is a self-synchronising codec). The first decoded sample MAY + contain a few ms of pre-roll noise; receivers SHOULD discard the + first decoded packet's pre-skip samples per RFC 7845 §4.2. ## Example diff --git a/nestsClient/specs/EGG-04.md b/nestsClient/specs/EGG-04.md index 85abd6c2c..0310aff08 100644 --- a/nestsClient/specs/EGG-04.md +++ b/nestsClient/specs/EGG-04.md @@ -43,10 +43,12 @@ The `a` tag is REQUIRED and points at the room defined in EGG-01. 1. A participant MUST emit a `kind:10312` event: - on entering the room, - - every 30 s while in the room, + - every 30 s ± 5 s of jitter while in the room, - immediately when any flag (`hand`, `muted`, `publishing`, `onstage`) changes, - one final time on leaving, with `publishing="0"` and `onstage="0"`. + The ±5 s jitter is REQUIRED. Synchronised heartbeats from a + thousand-listener room would otherwise trigger relay bursts every 30 s. 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.) @@ -69,6 +71,17 @@ The `a` tag is REQUIRED and points at the room defined in EGG-01. 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). +10. The flag values `"0"` and `"1"` are **strings**, not JSON booleans — + they appear inside a Nostr tag array which is `[string, string, ...]` + by NIP-01. Parsers MUST accept strings only; integers, booleans, or + any other JSON type for these positions MUST be treated as malformed. +11. **Single-room rule.** Because the `d`-tag is fixed (rule 7), a + participant has exactly one current presence event, scoped to one + room. Receivers MUST keep only the most recent `kind:10312` per + pubkey (replaceable-event semantics) and apply NIP-01 tie-break on + identical `created_at`. A buggy peer that publishes presences for + two rooms in rapid succession is presumed to be in whichever room + the most recent presence references. ## Example diff --git a/nestsClient/specs/EGG-05.md b/nestsClient/specs/EGG-05.md index c7b5cc63a..673a19623 100644 --- a/nestsClient/specs/EGG-05.md +++ b/nestsClient/specs/EGG-05.md @@ -47,6 +47,12 @@ The `content` field carries the plaintext (UTF-8) message. 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. +8. **Content size.** Senders SHOULD keep `content` under 8 KB of UTF-8. + Receivers MUST tolerate up to 64 KB and MAY truncate or drop messages + above that threshold (with a visible "[message truncated]" marker). +9. **Rate limiting.** Receivers SHOULD render at most 3 messages per + second per author in the live chat panel; over-quota messages remain + visible on scroll-back but do NOT animate / scroll the panel. ## Example diff --git a/nestsClient/specs/EGG-06.md b/nestsClient/specs/EGG-06.md index ccb8e1403..8af957c09 100644 --- a/nestsClient/specs/EGG-06.md +++ b/nestsClient/specs/EGG-06.md @@ -42,9 +42,11 @@ is room-wide (renders centered on the room canvas). 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. +2. Receivers MUST apply a 30-second sliding window measured against the + reaction's `created_at`: a reaction is rendered from the moment it is + observed until `created_at + 30 s` (wall-clock unix seconds), then + dropped from the floating overlay. A reaction that arrives already + older than 30 s MUST be dropped on receipt without ever being shown. 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. diff --git a/nestsClient/specs/EGG-07.md b/nestsClient/specs/EGG-07.md index eb3700dc8..e274fc8a4 100644 --- a/nestsClient/specs/EGG-07.md +++ b/nestsClient/specs/EGG-07.md @@ -80,6 +80,9 @@ the 60-second validity window defined below. - 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. + - The event's `id` MUST NOT have been seen in the last 120 s (replay + protection — relays may re-deliver the same event from multiple + peers, but a single kick MUST act exactly once per receiver). 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. diff --git a/nestsClient/specs/EGG-08.md b/nestsClient/specs/EGG-08.md index 2270affb0..57506284e 100644 --- a/nestsClient/specs/EGG-08.md +++ b/nestsClient/specs/EGG-08.md @@ -54,8 +54,11 @@ nearest second. 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"`. + MUST reject `POST /auth` requests targeting a `(host, room d)` whose + most-recent `kind:30312` is `status="planned"`, returning HTTP 403 + with `{"error":"room_closed"}` per the EGG-02 error taxonomy. (The + auth path becomes available the moment the host re-publishes with + `status="open"`; clients SHOULD NOT pre-mint tokens against `starts`.) 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). diff --git a/nestsClient/specs/EGG-10.md b/nestsClient/specs/EGG-10.md index c80a7ed16..119c0d679 100644 --- a/nestsClient/specs/EGG-10.md +++ b/nestsClient/specs/EGG-10.md @@ -48,6 +48,10 @@ Three optional tags on `kind:30312`: WEBP). - The mode is `cover` (scale to fill, crop overflow) or `tile` (repeat on both axes). Unknown modes MUST fall back to `cover`. +- Hosts SHOULD keep the asset under 1 MB and 4096 px on its longest + edge. Receivers MUST cap any asset they fetch at 8 MB and 8192 px on + its longest edge; over-cap assets MUST be discarded with a fall-through + to the `background` color so a malicious host cannot OOM clients. ## Behavior diff --git a/nestsClient/specs/README.md b/nestsClient/specs/README.md index 0e3abadf9..ad5e7d019 100644 --- a/nestsClient/specs/README.md +++ b/nestsClient/specs/README.md @@ -52,6 +52,108 @@ never re-purposed. EGG = Extensible Gossip Guideline. The acronym is intentional: nostrnests serves nests; nests hold eggs. +## Conventions + +Rules in this section apply across every EGG. They are normative. + +1. **Hex strings.** Every pubkey, event id, signature, and other hex value + referenced in any EGG MUST be **lowercase**, exactly the spec's expected + length (64 chars for pubkey/event id, 128 chars for sig), with NO `0x` + prefix and NO whitespace. Receivers MUST reject mixed-case or padded hex + rather than silently lowercasing — an inconsistent hex on the wire is a + bug at the source. +2. **Nostr foundations.** Unless an EGG says otherwise, every event referenced + here is a NIP-01 event (id = sha256 of the canonical serialization; + schnorr signature over `id`). Receivers MUST verify `id` and `sig` before + acting on the event. Replaceable / addressable / ephemeral semantics + follow NIP-01. +3. **`a`-tag form.** Every `["a", ...]` tag in these specs uses the + addressable form `::`. The optional 3rd + element is a relay hint URL. +4. **Created-at tie-break.** When two events share the same `(kind, pubkey, + d)` AND the same `created_at`, receivers MUST keep the one with the + lexicographically SMALLEST event id and discard the other (matches NIP-01 + replaceable-event tie-break behavior). +5. **JSON.** All HTTP bodies and event content are UTF-8. JSON producers MUST + NOT include a BOM. JSON parsers MUST tolerate trailing whitespace. +6. **Time.** All timestamps are unsigned unix seconds (base-10 ASCII when + carried as a tag value, integer when carried in JSON). Sub-second + precision is out of scope. + +## Joining sequence + +A normative end-to-end walkthrough for "I have a `kind:30312` event in hand, +get me to first audio frame". Every step references the EGG that defines it. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ 1. Parse room event (EGG-01) │ +│ - Verify NIP-01 id + sig │ +│ - Read `service`, `endpoint`, `relays`, `status`, `p` tags │ +│ - Reject if status != "open" / "private" │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 2. Subscribe to chat / presence relays (EGG-04, EGG-05, EGG-06) │ +│ Filter: { "#a": ["30312::"], "kinds":[1311,7,4312] } │ +│ Filter: { "#a": ["30312::"], "kinds":[10312] } │ +│ Relays = `relays` tag ∪ host's NIP-65 outbox │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 3. Mint moq-auth JWT (EGG-02) │ +│ POST /auth │ +│ Authorization: Nostr │ +│ body: { "namespace": "nests/30312::", │ +│ "publish": } │ +│ ← 200 { "token": "" } │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 4. Open WebTransport session (EGG-02) │ +│ CONNECT :path = /?jwt= │ +│ :authority = host:port from `endpoint` │ +│ Run moq-lite Setup handshake on the bidi control stream │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 5. Discover speakers (EGG-03) │ +│ Send moq-lite AnnouncePlease prefix="" │ +│ Each Announce { suffix=, status=Active } │ +│ is one live speaker. │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 6. Subscribe per speaker (EGG-03) │ +│ Subscribe { broadcast=, track="audio/data" } │ +│ Each Group → unidirectional QUIC stream │ +│ Each Frame → exactly one 20 ms Opus packet │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 7. Publish own presence (EGG-04) │ +│ kind:10312 with `a` = "30312::", flags, fixed d-tag │ +│ Re-publish every 30 s (with ±5 s jitter) until leaving │ +└──────────────────────────────────────────────────────────────────────┘ + │ + ▼ + (speaker only) +┌──────────────────────────────────────────────────────────────────────┐ +│ 8. Speaker — publish own audio (EGG-03) │ +│ Send moq-lite Announce { suffix=, status=Active } │ +│ Open one unidirectional QUIC stream per Group, write Frames │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +Token lifetime is 600 s. Re-mint a fresh token (steps 3 + 4) before the +old one expires. Relays MUST close sessions within 30 s past `exp`. + ## Filing changes Edit a single EGG per pull request. Include a wire-format example showing the