Files
amethyst/nestsClient/specs/nip-53-proposed.md
Claude aadb347b84 feat(nests): adopt deployed nostrnests schema (streaming/auth/live + paired 10112)
The deployed nostrnests reference (NestsUI v2 + moq-auth + moq-rs)
emits a different on-the-wire schema than the previous EGG-01 / EGG-09
drafts and Quartz writers. Verified by reading the production
NestsUI bundle. The deployed schema is now canonical:

kind:30312 (room event)
  - relay URL → ["streaming", url]   (was ["endpoint", url])
  - auth URL  → ["auth",      url]   (was ["service",  url])
  - live status → ["status", "live"]   (was "open")
  - ended status → ["status", "ended"] (was "closed")
  - room name → ["title", name]        (was ["room", name])

kind:10112 (user MoQ-server list)
  - 3-element entries: ["server", relay, auth]
  - first-element name "relay" accepted as a synonym (legacy)
  - 2-element entries: derive auth host by replacing leading "moq."
    with "moq-auth.", or prepending "moq-auth." otherwise

Implementation
- ServiceUrlTag.TAG_NAME = "auth", LEGACY_TAG_NAME = "service"
- EndpointUrlTag.TAG_NAME = "streaming", LEGACY_TAG_NAME = "endpoint"
- StatusTag enum: PLANNED/LIVE/PRIVATE/ENDED with code "live"/"ended";
  legacy "open"/"closed" still parsed on read
- NestsServersEvent: emit/read 3-element [server, relay, auth] tags
  with the legacy-shape tolerances above; expose a NestsServer pair
- Account.nestsServers.flow now produces List<NestsServer> pairs
- NestsServersScreen: rewritten edit-field asks for both URLs,
  recommended row shows the nostrnests pair, list rows show both URLs
- NestsScreen first-time setup writes the nostrnests pair, not a
  single URL; gate the create FAB on both URLs being parseable
- CreateNestViewModel: drop the resolveServerPair hardcoded mapping —
  the saved pair is now authoritative; defaults stay correct for an
  empty kind-10112 list

Specs
- EGG-01 rewritten to canonical streaming/auth/live/ended with a
  legacy-spelling table; example uses the real nostrnests pair
- EGG-02 references "auth" tag throughout; error taxonomy says "ended"
- EGG-09 rewritten to 3-element server tag with derivation fallback
- New nestsClient/specs/nip-53-proposed.md — a single self-contained
  proposed update to upstream NIP-53 covering kind 30312 + kind 10112
  with the deployed schema and the JWT-mint flow

All existing unit tests adjusted; quartz:jvmTest, amethyst:testPlayDebugUnitTest,
and nestsClient:jvmTest pass.
2026-04-29 19:32:53 +00:00

350 lines
13 KiB
Markdown

# NIP-53
## Live Activities
`draft` `optional`
Service providers want to offer live activities to the Nostr network in such a way that participants can easily logon, chat, send zaps and follow other participants. This NIP describes a framework to advertise and discover hosts of live activities, such as streaming and audio rooms.
## Format
The format uses two NIP-01 *addressable* events, plus one *replaceable* event for user preferences:
| kind | name | scope |
|-------|---------------------------------|----------------------|
| 30311 | Live Event | streaming |
| 30312 | Interactive Room (Audio Space) | nests / audio rooms |
| 10112 | User MoQ-Server List | nests user prefs |
This document is a **proposed update to NIP-53** based on what the
deployed nostrnests reference (NestsUI v2 + moq-auth + moq-rs)
actually emits. Earlier versions of this NIP described kind 30312
with the `service`, `endpoint`, and `status: open|closed` tags; the
deployed reference uses `auth`, `streaming`, and `status: live|ended`
instead. Receivers MUST tolerate both tag-name spellings (canonical
vs. legacy) but publishers MUST emit the canonical names defined
below.
### Live Event (kind 30311)
Unchanged from the current NIP-53. Streaming hosts publish a
NIP-01 addressable event with the following tags:
```json
{
"kind": 30311,
"tags": [
["d", "<unique identifier>"],
["title", "<name of the event>"],
["summary", "<description>"],
["image", "<preview image url>"],
["t", "hashtag"],
["streaming", "<url>"],
["recording", "<url>"],
["starts", "<unix timestamp in seconds>"],
["ends", "<unix timestamp in seconds>"],
["status", "<planned, live, ended>"],
["current_participants", "<number>"],
["total_participants", "<number>"],
["p", "<pubkey>", "<relay url>", "<role>", "<proof>"],
["relays", "<relay 1>", "<relay 2>", ...]
]
}
```
### Interactive Room (kind 30312)
A `kind:30312` is the audio-room (a.k.a. **nest**) counterpart of a
streaming Live Event. The event's pubkey is the host. Subscribers read
this event to learn who runs the room, where the audio plane lives,
who else is invited, and whether the room is currently live.
```json
{
"kind": 30312,
"pubkey": "<host pubkey hex>",
"tags": [
["d", "<room id>"],
["title", "<room name>"],
["summary", "<one-line description>"],
["image", "<optional cover image URL>"],
["status", "live" | "private" | "ended" | "planned"],
["streaming", "<https URL of moq-relay WebTransport>"],
["auth", "<https URL of moq-auth sidecar>"],
["starts", "<unix timestamp in seconds>"],
["color", "<gradient identifier or hex>"],
["relays", "<wss relay 1>", "<wss relay 2>", ...],
["p", "<pubkey>", "<relay hint>", "host" | "admin" | "speaker"],
...
],
"content": "",
...
}
```
Required tags: `d`, `title`, `status`, `streaming`, `auth`, plus a
single `p` entry naming the host.
#### `streaming` tag
Base URL of a WebTransport-capable moq-relay (the audio plane). The
relay listens on QUIC; the URL's port may accept UDP only. Clients
MUST NOT issue HTTP requests against this URL — it is the
WebTransport authority for the `:authority` pseudo-header during
session establishment.
#### `auth` tag
Base URL of the moq-auth sidecar (the JWT mint). The sidecar speaks
HTTP/1.1 or HTTP/2 over TCP/TLS — it MUST be reachable on a port
that accepts TCP (typically 443). Clients MUST NOT collapse this URL
into the `streaming` URL: the public reference deployment hosts them
on different hosts (`moq.nostrnests.com:4443` for the relay,
`moq-auth.nostrnests.com` for auth), and an HTTP request against the
QUIC-only relay port hangs without ever reaching a server.
The two URLs MAY point at the same authority for community
deployments that genuinely co-locate both services, but they remain
independent fields and clients MUST NOT assume they share an
authority.
#### `status` tag
| value | meaning |
|-----------|--------------------------------------------------------------------|
| `live` | Room in progress, anyone may join. Auth sidecar mints listener tokens. |
| `private` | Room in progress with an out-of-band allowlist. Auth sidecar replies `403` to non-allowlisted requesters. |
| `ended` | Room is over; audio plane SHOULD be torn down server-side. |
| `planned` | Room is scheduled to start at the `starts` unix-second timestamp. |
Receivers MUST also accept `open` (synonym for `live`) and `closed`
(synonym for `ended`) on read for back-compat with earlier drafts.
Publishers MUST emit only the canonical names.
#### `p` tag (participant)
`["p", <pubkey hex>, <relay hint>, <role>, <proof>]`
`role` is one of `host`, `admin`, `speaker`. Role flips re-publish
the kind-30312 event. `proof` is reserved for invitee
acknowledgements; absent today.
#### `relays` tag
A SINGLE tag whose first element is the literal string `"relays"`
and whose remaining elements are wss URLs. Implementers MUST NOT
emit one `["relays", url]` tag per relay.
#### Authentication & WebTransport handshake
To open the audio plane, a peer mints a JWT against the auth sidecar
using NIP-98 and then opens a WebTransport CONNECT against the relay
carrying the JWT in the URL.
##### Step 1 — token request
```
POST <auth>/auth
Authorization: Nostr <base64(NIP-98 kind:27235 event JSON)>
Content-Type: application/json; charset=utf-8
{ "namespace": "nests/30312:<host_pubkey_hex>:<room_d>",
"publish": <boolean> }
```
`<auth>` is the kind-30312 `auth` tag value with any trailing `/`
stripped. The body is a single-line UTF-8 JSON object — the server
hashes the **exact bytes sent** to compare against NIP-98's `payload`
tag, so producers MUST NOT pretty-print or re-order keys after
signing. The NIP-98 event's `u` tag MUST be the full URL
`<auth>/auth` (scheme included).
`publish: true` requests a speaker token; `publish: false` a listener
token. The auth sidecar MUST gate `publish: true` on the requester
appearing in the kind-30312 event's `p` tag with role `host`,
`admin`, or `speaker`.
##### Step 2 — token response
```
HTTP/1.1 200 OK
Content-Type: application/json
{ "token": "<jwt>" }
```
The JWT is signed `alg: ES256`. The sidecar MUST publish its public
keys at `GET <auth>/.well-known/jwks.json` (RFC 7517). The relay
MUST verify inbound JWTs against this JWKS and SHOULD cache it for
at most 5 minutes.
JWT claims (minimum):
| claim | meaning |
|---------|--------------------------------------------------------------------|
| `root` | The `namespace` echoed back. Authorization is scoped here. |
| `get` | Read-allowed sub-paths; for nests this is `[""]` (any). |
| `put` | Publish-allowed sub-paths. Listener: `[]`. Speaker: `[<pubkey>]`. |
| `iat` | Unix seconds; issuance. |
| `exp` | Unix seconds; expiry. Recommended `iat + 600`. |
There is no refresh endpoint. Long sessions MUST mint a fresh token
and open a new WebTransport session before the old token expires.
##### Step 3 — WebTransport CONNECT
```
:method = CONNECT
:protocol = webtransport
:scheme = https
:authority = <host:port from `streaming`>
:path = /<namespace>?jwt=<token>
```
The relay reads the JWT from the `?jwt=` query parameter and MUST
NOT trust the WebTransport authority for authorization — only the
JWT's `root` claim. A token issued for namespace A MUST NOT be
accepted on a session opened against namespace B.
Peers MUST NOT log the JWT or include it in error reports.
##### Error taxonomy
The auth sidecar MUST return `application/json` bodies of shape
`{ "error": "<slug>", "reason": "<human string>" }` on non-2xx
responses. Defined slugs:
| status | slug | when |
|--------|---------------------|------------------------------------------------------------------|
| 400 | `bad_request` | malformed JSON body or missing `namespace` |
| 400 | `bad_namespace` | namespace does not match `nests/<kind>:<hexpubkey>:<d>` |
| 401 | `bad_nip98` | Authorization header missing, malformed, or `id`/`sig` invalid |
| 401 | `wrong_url` | NIP-98 `u` tag does not match the actual request URL |
| 401 | `wrong_method` | NIP-98 `method` tag is not `POST` |
| 401 | `wrong_payload` | NIP-98 `payload` tag does not match sha256 of the body bytes |
| 401 | `stale` | NIP-98 `created_at` outside the ±60 s tolerance |
| 403 | `room_closed` | room status is `ended` or `planned` |
| 403 | `not_invited` | room status is `private` and requester not on allowlist |
| 403 | `publish_forbidden` | `publish: true` requested but caller is not a speaker |
| 410 | `unknown_room` | no `kind:30312` known to the sidecar for `(host, d)` |
| 429 | `rate_limited` | per-pubkey or per-IP rate limit; `Retry-After` SHOULD be set |
| 5xx | `internal` | sidecar internal error |
The relay signals authorization failures through WebTransport
CONNECT response codes:
| WT status | meaning |
|-----------|------------------------------------------------------------------------|
| 200 | session established |
| 401 | JWT signature invalid, expired, or fails the JWKS check |
| 403 | JWT `root` does not match path namespace, or `put` claim missing |
| 404 | path namespace unknown to the relay |
#### Example
```json
{
"kind": 30312,
"pubkey": "abc...host",
"created_at": 1714003200,
"tags": [
["d", "office-hours-2026-04"],
["title", "Office Hours"],
["summary", "Weekly Q&A"],
["status", "live"],
["streaming", "https://moq.nostrnests.com:4443"],
["auth", "https://moq-auth.nostrnests.com"],
["p", "abc...host", "wss://relay.example", "host"],
["p", "def...co", "wss://relay.example", "speaker"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
### User MoQ-Server List (kind 10112)
A user MAY publish a list of nests servers they prefer to host on as
a NIP-01 *replaceable* event. Clients read this list to default-fill
the `streaming` and `auth` tags of a new `kind:30312` room.
Each entry is a single tag carrying **two** URLs because the
moq-relay and moq-auth sidecar live on different hosts in the
deployed reference:
```json
{
"kind": 10112,
"pubkey": "<user pubkey hex>",
"tags": [
["server", "<https URL of moq-relay>", "<https URL of moq-auth>"],
["server", "<https URL of moq-relay>", "<https URL of moq-auth>"],
...
],
"content": ""
}
```
Tag elements are positional: index 1 is the moq-relay URL (becomes
the kind-30312 `streaming` tag value), index 2 is the moq-auth URL
(becomes the kind-30312 `auth` tag value). Order of tags is
preserved by receivers; earlier entries are higher priority.
#### Back-compat
Receivers MUST accept two looser shapes from earlier deployed
clients; publishers MUST emit only the canonical 3-element form:
1. **First-element name `relay`.** Earliest NestsUI iterations
wrote `["relay", relay, auth]`. Treat as a synonym for `server`.
2. **Auth URL omitted.** A 2-element `["server", relay]` (or
`["relay", relay]`) carries no auth URL on the wire. Receivers
MUST derive it from the relay URL by replacing a leading `moq.`
host label with `moq-auth.`, or prepending `moq-auth.` when the
relay host has no `moq.` prefix. Drop the entry if the relay URL
is unparseable.
#### Behavior
1. Each `relay` and `auth` value MUST be a fully-qualified URL
beginning with `https://`. Receivers MUST drop entries that are
not well-formed HTTPS URLs.
2. Receivers MUST de-duplicate by exact-string match on the relay
URL after trimming a single trailing `/`. Order is preserved
(FIRST occurrence wins).
3. When the user opens a create-room sheet, the client SHOULD
pre-fill the kind-30312 `streaming` and `auth` tags from the
FIRST entry in the list. Clients MUST NOT collapse the two URLs
into a single field.
4. Users MAY enumerate up to 64 servers; receivers MUST tolerate
longer lists by truncating to the first 64.
5. The list is purely a defaults / discovery hint. A `kind:30312`
event's own `streaming` / `auth` tags are authoritative for that
specific room and override the user list at join time.
#### Example
```json
{
"kind": 10112,
"pubkey": "abc...host",
"created_at": 1714003000,
"tags": [
["server", "https://moq.nostrnests.com:4443", "https://moq-auth.nostrnests.com"],
["server", "https://relay.example.org:4443", "https://moq-auth.example.org"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
## Use Cases
Common use cases include meeting room/conference calls, watch-together activities, audio spaces, hangouts, and game streams.
## Notes
Live Activity management events are not designed to be used by relays for filtering, so clients SHOULD use the addressable event (`<kind>:<pubkey>:<d>`) when referencing room messages.