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:
@@ -89,6 +89,27 @@ publishers that emit it are non-conformant.
|
||||
share `(pubkey, d)` AND `created_at`, receivers MUST keep the event
|
||||
with the lexicographically SMALLEST `id` and discard the other (per
|
||||
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
|
||||
|
||||
|
||||
@@ -51,6 +51,32 @@ the 60-second validity window defined below.
|
||||
|
||||
## 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
|
||||
|
||||
1. To promote a listener to speaker, the host or an admin MUST re-publish
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Edit a single EGG per pull request. Include a wire-format example showing the
|
||||
|
||||
Reference in New Issue
Block a user