docs(nestsClient/specs): close hosting-side ambiguities

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
This commit is contained in:
Claude
2026-04-27 13:05:49 +00:00
parent bd12d5ef72
commit fd03122206
3 changed files with 128 additions and 0 deletions
+21
View File
@@ -89,6 +89,27 @@ publishers that emit it are non-conformant.
share `(pubkey, d)` AND `created_at`, receivers MUST keep the event share `(pubkey, d)` AND `created_at`, receivers MUST keep the event
with the lexicographically SMALLEST `id` and discard the other (per with the lexicographically SMALLEST `id` and discard the other (per
NIP-01 replaceable-event tie-break). NIP-01 replaceable-event tie-break).
12. **Publish-before-mint ordering.** A peer MUST NOT request a JWT
(EGG-02) for a room before that room's `kind:30312` event has
propagated to relays the auth sidecar can read. The auth sidecar
looks up the most-recent `kind:30312` by `(pubkey, kind, d)` to
validate the room exists and to enforce status-based gates
(EGG-08 rule 5). For hosts: this means publishing the freshly-
composed event BEFORE minting their own `publish: true` token.
For listeners: this means waiting until the event is visible on
the listener's own relay set (an unknown room cannot be joined).
Auth sidecars that cannot find the room MUST return HTTP 410
`unknown_room` (EGG-02 error taxonomy); peers SHOULD retry with
1 s / 2 s / 4 s exponential backoff before surfacing the error.
13. **Service / endpoint selection (host-side guidance).** When
composing a new room, hosts SHOULD pre-fill `service` and
`endpoint` from the FIRST entry of their own `kind:10112` user
server list (EGG-09). When the host has no `kind:10112`, the
client MAY ship a built-in default URL but MUST allow user
override. Both `service` and `endpoint` MUST be `https://` URLs;
`http://` MUST be rejected at compose time (mirrors EGG-09
rule 1). The `service` and `endpoint` MAY be the same base URL
(a single deployment serving both is the common case).
## Example ## Example
+26
View File
@@ -51,6 +51,32 @@ the 60-second validity window defined below.
## Behavior ## Behavior
### Audio publish authorisation
The auth sidecar (EGG-02) MUST grant a `publish: true` JWT — with the
caller's pubkey hex listed in the JWT's `put` claim — to any peer that
is one of:
- the **host** (i.e. the author of the most-recent `kind:30312` event
for the requested `(kind, d)`), regardless of whether the host's own
`p`-tag carries a `"host"`, `"speaker"`, or no role marker. The host's
speaker capability is implicit in event authorship — clients MUST NOT
require the host to add themselves as `["p", _, _, "speaker"]` in
order to broadcast,
- a peer marked `["p", <self>, _, "speaker"]` in that same event,
- a peer marked `["p", <self>, _, "admin"]` in that same event.
Other callers requesting `publish: true` MUST receive HTTP 403
`publish_forbidden` per the EGG-02 error taxonomy. A `publish: false`
(listener) request MUST be accepted from any well-formed NIP-98 caller
when the room is `open` (per EGG-01 rule 6).
The relay (EGG-03 endpoint) is independently authorised by the JWT's
`put` claim — it does NOT re-read the `kind:30312` event. Demoting a
speaker therefore does NOT terminate their existing audio session;
the speaker keeps publishing until either their JWT expires or the
host kicks them via EGG-07's kick command.
### Promote / demote ### Promote / demote
1. To promote a listener to speaker, the host or an admin MUST re-publish 1. To promote a listener to speaker, the host or an admin MUST re-publish
+81
View File
@@ -154,6 +154,87 @@ get me to first audio frame". Every step references the EGG that defines it.
Token lifetime is 600 s. Re-mint a fresh token (steps 3 + 4) before the 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`. old one expires. Relays MUST close sessions within 30 s past `exp`.
## Hosting a new room
A normative walkthrough for "I am a host; create a fresh room and start
broadcasting". Symmetrical to the joining sequence above but starts one
step earlier — the host has to bring the `kind:30312` into existence
before anyone (including themselves) can authenticate against it.
```
┌──────────────────────────────────────────────────────────────────────┐
│ 1. Pick service + endpoint (EGG-01, EGG-09) │
│ Default: FIRST entry of host's own kind:10112 (EGG-09) │
│ Both MUST be https://; http:// is rejected (EGG-09 rule 1) │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Compose kind:30312 (EGG-01) │
│ - status = "open" (or "planned" + starts tag, EGG-08) │
│ - d = fresh room id, charset [A-Za-z0-9._-] (EGG-01 rule 9) │
│ - first p-tag = ["p", <own pubkey>, <relay hint>, "host"] │
│ - relays / image / summary / theme tags as desired │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 3. Sign + publish to relays (EGG-01 rule 10) │
│ Target set = `relays` tag host's NIP-65 outbox │
│ The auth sidecar's inbound relay pool MUST overlap this set, │
│ otherwise step 4 will 410 `unknown_room` indefinitely. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 4. Mint publish JWT (EGG-02) │
│ POST <service>/auth, body = {namespace, "publish": true} │
│ NIP-98 Authorization signed by host pubkey. │
│ On 410 `unknown_room`: relay-propagation lag — retry up to 3 │
│ times with backoff (1s / 2s / 4s) before surfacing. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 5. Open WebTransport + run moq-lite Setup (EGG-02 step 3) │
│ Same as listener flow. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 6. Announce own broadcast (EGG-03) │
│ Announce { suffix=<own pubkey hex>, status=Active, hops=0 } │
│ Then AnnouncePlease prefix="" to also receive other speakers. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 7. Publish own presence (EGG-04) │
│ publishing="1", onstage="1", muted="0"; heartbeat per EGG-04. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 8. Stream Opus (EGG-03) │
│ One unidirectional QUIC stream per Group, one Opus packet per │
│ Frame, 20 ms cadence. │
└──────────────────────────────────────────────────────────────────────┘
```
Ongoing host duties:
- **Add a speaker:** re-publish kind:30312 with the new `["p", _, _,
"speaker"]` tag (EGG-07).
- **Promote to admin / demote / remove:** re-publish kind:30312 with
updated role markers (EGG-07).
- **Kick:** sign and broadcast a kind:4312 ephemeral (EGG-07).
- **Edit metadata** (rename, image, theme): re-publish kind:30312 with
higher `created_at` (EGG-01 rule 1).
- **Close the room:** re-publish kind:30312 with `status="closed"`
(EGG-01 rule 6). The audio plane SHOULD be torn down server-side.
- **Publish recording** after close: re-publish closed event with
`["recording", url]` tag (EGG-11).
## Filing changes ## Filing changes
Edit a single EGG per pull request. Include a wire-format example showing the Edit a single EGG per pull request. Include a wire-format example showing the