Files
amethyst/nestsClient/specs/EGG-07.md
T
Claude bd12d5ef72 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
2026-04-27 12:31:19 +00:00

4.8 KiB

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)

{
  "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

  1. 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.
  2. 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.
    • 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.
  3. The TARGET of a kick MUST disconnect from the audio plane (close the moq-lite session) and tear down the in-room UI.
  4. 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.
  5. The relay MUST NOT enforce kicks. Authorization is purely client-side and signature-based.

Example

Host promotes a listener to speaker:

{
  "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:

{
  "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.