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:
@@ -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