docs(nestsClient/specs): close interop gaps surfaced in spec review
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
This commit is contained in:
@@ -26,7 +26,7 @@ Every other EGG layers on top of this event.
|
||||
["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>", ...],
|
||||
["relays", "<wss relay 1>", "<wss relay 2>", ...], // ONE tag, multiple values
|
||||
["p", "<pubkey>", "<relay hint>", "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/<kind>:<host pubkey hex>:<d>` (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
|
||||
|
||||
|
||||
+104
-4
@@ -20,8 +20,8 @@ sidecar; the `endpoint` tag is the relay.
|
||||
|
||||
```
|
||||
POST <service>/auth
|
||||
Authorization: Nostr <base64(NIP-98 kind:27235 event)>
|
||||
Content-Type: application/json
|
||||
Authorization: Nostr <base64(NIP-98 kind:27235 event JSON)>
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{ "namespace": "nests/<kind>:<host_pubkey_hex>:<room_d>",
|
||||
"publish": <boolean> }
|
||||
@@ -29,8 +29,41 @@ Content-Type: application/json
|
||||
|
||||
`<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.
|
||||
The `<service>` 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 `<host_pubkey_hex>` is the host's pubkey, lowercase hex. The `<room_d>`
|
||||
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": "<requester pubkey hex>",
|
||||
"created_at": <unix seconds>,
|
||||
"tags": [
|
||||
["u", "<service>/auth"], // exact request URL, scheme included
|
||||
["method", "POST"],
|
||||
["payload", "<sha256 of request body, lowercase hex>"]
|
||||
],
|
||||
"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 <service>/.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": "<key id>",
|
||||
"x": "<base64url x>", "y": "<base64url 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": "<machine slug>", "reason": "<human string>" }`. Receivers
|
||||
MAY ignore `reason` but MUST surface `error` to user-facing error toasts.
|
||||
|
||||
| status | `error` slug | when |
|
||||
|--------|----------------------|---------------------------------------------------------------------|
|
||||
| 200 | — | success; body is `{ "token": "<jwt>" }` |
|
||||
| 400 | `bad_request` | malformed JSON body, missing `namespace`, unknown content-type |
|
||||
| 400 | `bad_namespace` | namespace does not match `nests/<kind>:<hexpubkey>:<d>` |
|
||||
| 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
|
||||
|
||||
```
|
||||
|
||||
@@ -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 = <pubkey hex>
|
||||
status = Active
|
||||
suffix = <pubkey hex> (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 = <speaker pubkey hex>
|
||||
broadcast = <speaker pubkey hex> (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 `<pubkey hex>`.
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 `<kind>:<author pubkey hex>:<d-tag>`. 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:<host>:<d>"], "kinds":[1311,7,4312] } │
|
||||
│ Filter: { "#a": ["30312:<host>:<d>"], "kinds":[10312] } │
|
||||
│ Relays = `relays` tag ∪ host's NIP-65 outbox │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Mint moq-auth JWT (EGG-02) │
|
||||
│ POST <service>/auth │
|
||||
│ Authorization: Nostr <base64(NIP-98 event)> │
|
||||
│ body: { "namespace": "nests/30312:<host>:<d>", │
|
||||
│ "publish": <true if speaker, else false> } │
|
||||
│ ← 200 { "token": "<es256 jwt>" } │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 4. Open WebTransport session (EGG-02) │
|
||||
│ CONNECT :path = /<namespace>?jwt=<token> │
|
||||
│ :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=<speaker pubkey hex>, status=Active } │
|
||||
│ is one live speaker. │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Subscribe per speaker (EGG-03) │
|
||||
│ Subscribe { broadcast=<pubkey hex>, 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:<host>:<d>", 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=<own pubkey hex>, 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
|
||||
|
||||
Reference in New Issue
Block a user