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:
Claude
2026-04-27 12:31:19 +00:00
parent 5c40e27fde
commit bd12d5ef72
10 changed files with 325 additions and 22 deletions
+102
View File
@@ -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