After verifying the nostrnests reference (NestsUI-v2 @ main):
- ProfileCard.tsx writes kicks as ['action','kick'] tags with empty content
- useAdminCommands.ts reads action via tags.find(t => t==='action') and
applies a 60-s relay since plus a processedRef Set to dedup re-deliveries
- p-tag role marker for moderators is 'admin', not 'moderator'
Amethyst was diverging on every one of those, which means our outbound
admin commands were invisible to nostrnests, theirs to us, and any
nostrnests admin (role='admin') failed our isModerator() / canSpeak()
gates entirely — kicks and force-mutes signed by them were silently
dropped.
Changes:
quartz/AdminCommandEvent.kt
- Emit ['action', '<verb>'] tag with empty content
- Reader prefers the tag, falls back to content for any in-flight
Amethyst-built kick from before this commit
- kick() and forceMute() share a common build() helper
quartz/ParticipantTag.kt
- ROLE.MODERATOR.code = 'admin' (matches nostrnests + EGG-07)
- Adds legacyCodes = ['moderator'] so older Amethyst-emitted
kind-30312 events still parse as MODERATOR
- effectiveRole() walks both code + legacyCodes
amethyst/AdminCommandsCollector
- Filter carries since = now - 60 (EGG-07 #7)
- Defensive per-event freshness re-check for cached events / clock skew
- mutableSetOf<String>() processed-id dedup for the lifetime of the
collector, mirroring useAdminCommands.ts's processedRef
EGG-07.md
- Documents the Amethyst-only ['action','mute'] extension under a new
'Implemented extensions' section. nostrnests doesn't emit or honour
it today; cross-client force-mutes only work between Amethyst peers
- 'warn' stays in the future-actions list — nostrnests has no plans
for it either
Tests:
- ParticipantTagTest: new asserts that 'admin' / 'Admin' / 'ADMIN'
parse as ROLE.MODERATOR; pins the wire string to 'admin' and the
legacy alias to 'moderator'
- AdminCommandEventTest: kick/forceMute templates carry ['action', _]
tag with empty content; legacy content-form still parses; tag wins
over content when both are present
Define the host-side flow that was previously implicit. A new-room
implementer can now go from "I want to start broadcasting" to first
audio frame without reading our source.
README — new "Hosting a new room" section:
- 8-step ASCII walkthrough symmetrical to "Joining sequence".
- Covers service/endpoint selection, kind:30312 compose + publish, JWT
mint with publish:true, WT/Setup, Announce, presence, Opus stream.
- Lists ongoing host duties (add speaker, kick, edit, close, recording).
EGG-01 — two new behavior rules:
- Rule 12 "Publish-before-mint ordering": peers MUST publish the
kind:30312 to relays BEFORE requesting a JWT for it, since the auth
sidecar reads the most-recent event by (pubkey, kind, d) to validate
existence / status. 410 unknown_room → retry 1s/2s/4s. Closes the
silent footgun where a host could mint a token before their event
has propagated.
- Rule 13 "Service/endpoint selection (host-side guidance)": pre-fill
from kind:10112 first entry, fall back to client default with user
override, both MUST be https://.
EGG-07 — new "Audio publish authorisation" section:
- Spells out who gets a publish:true JWT: host (by authorship,
implicitly — no need for ["p", _, _, "speaker"] on the host's own
p-tag), explicit "speaker" role, or "admin" role. All others 403
publish_forbidden per EGG-02.
- Relay does NOT re-read kind:30312; demoting a speaker mid-session
does NOT terminate their stream — host MUST kick (kind:4312) to
end an active broadcast.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
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
Wire-protocol specs for nostrnests-style audio rooms, in the
style of Nostr NIPs and Blossom BUDs. Each spec documents one
self-contained capability that a client or relay can implement;
two compliant peers implementing the same set of EGGs round-trip
without further coordination.
Layout:
README.md cover, status table, conformance levels
EGG-01.md Room event (kind:30312) required
EGG-02.md Auth + WebTransport handshake required
EGG-03.md Audio plane (moq-lite) required
EGG-04.md Presence (kind:10312) required
EGG-05.md In-room chat (kind:1311) optional
EGG-06.md Reactions (kind:7) optional
EGG-07.md Roles & moderation (kind:4312) optional
EGG-08.md Scheduling (status=planned) optional
EGG-09.md User server list (kind:10112) optional
EGG-10.md Theming (c/f/bg tags) decorative
EGG-11.md Recording decorative
EGG-12.md Catalog track (catalog.json) optional
Conformance levels (Listener / Speaker / Host) defined in the
README so a deployment can declare "we implement EGG-01..EGG-04"
and other peers know exactly what to expect.
Each spec follows the same shape (Summary / Wire format /
Behavior numbered MUST/SHOULD/MAY rules / Example /
Compatibility) and fits on a single printed page. Wire formats
are documented exactly as nostrnests + amethyst implement them
on this branch — no hypothetical capabilities, no "future" tags
without an EGG number.