Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
f54c423
quest: open the auth line
kixelated Sep 24, 2026
288ea4f
feat(net): exchange grants over lite-06 AUTH streams (#4062)
kixelated Sep 25, 2026
1c21a2e
Merge remote-tracking branch 'origin/main' into quest/m1/auth/README
kixelated Sep 25, 2026
615f843
feat(net): exchange grants over the moq-transport MoQ Auth extension …
kixelated Sep 25, 2026
600cc6b
Merge origin/main into quest/m1/auth/README
kixelated Sep 25, 2026
5304553
Merge origin/main into quest/m1/auth/README
kixelated Sep 25, 2026
6f51a4a
quest(auth): path patterns join the line so AUTH ships with pattern g…
kixelated Sep 26, 2026
29463bf
test(interop): assert each AUTH cell's grant and refuse a publish out…
kixelated Sep 26, 2026
0bb0aa2
feat(net): reset revoked lite streams with UNAUTHORIZED (#4179)
kixelated Sep 26, 2026
09675cc
Merge origin/main into quest/m1/auth/README
kixelated Sep 26, 2026
404d05c
fix(net): a withdrawn token ends with Cancel even if the acceptor's F…
kixelated Sep 26, 2026
3fdc123
feat(net): carry path patterns in moq-lite AUTH_OK grants (#4277)
kixelated Sep 27, 2026
ea7e984
Merge origin/main into quest/m1/auth/README
kixelated Sep 28, 2026
c534db8
chore(quest): claim auth-ok-preflight
kixelated Sep 29, 2026
4b927ac
fix(net): refuse an AUTH_OK that cannot be encoded, on any sizing error
kixelated Sep 29, 2026
ee275b2
fix(net): mirror the lite 64 MiB encode ceiling in JS
kixelated Sep 29, 2026
acc9a3d
docs(net): say what a lite auth reset means after a grant
kixelated Sep 29, 2026
240a2b8
chore(quest): claim quest/m1/auth/error-codes
kixelated Sep 29, 2026
4186b50
quest(auth/narrowing): claim
kixelated Sep 29, 2026
c63b1d1
fix(net): make every AUTH stream ending exact on both sides
kixelated Sep 29, 2026
360c191
Merge pull request #4446 from moq-dev/quest/m1/auth/auth-ok-preflight
kixelated Sep 29, 2026
c82f9c7
quest(auth): AUTH protocol violations close the session everywhere
kixelated Sep 29, 2026
7e6dd2f
feat(net): narrow a live session's grant in place
kixelated Sep 29, 2026
7bc0dd0
Merge branch 'quest/m1/auth/README' into quest/m1/auth/error-codes
kixelated Sep 29, 2026
8b26a0e
quest(auth): cover a malformed inbound AUTH too
kixelated Sep 29, 2026
e34649c
feat(relay): narrow a live session on a narrower re-check
kixelated Sep 29, 2026
e2891ba
fix(net): arm JS grant watches before a request's first await
kixelated Sep 29, 2026
39d5f08
fix(net): log the AUTH_ERROR code limit, and drop the raw timer in it…
kixelated Sep 29, 2026
47abdbb
quest(auth/narrowing): done; bench the narrowing and its gates
kixelated Sep 29, 2026
d0d560a
Merge remote-tracking branch 'origin/quest/m1/auth/README' into quest…
kixelated Sep 29, 2026
196bb9a
chore: fmt and doc touch-ups
kixelated Sep 29, 2026
f211e7b
quest(auth): validate the IETF AUTH request ID too
kixelated Sep 29, 2026
7e773ee
test(relay): iterate subscriptions directly
kixelated Sep 29, 2026
5af6279
quest(auth): unknown reply types and a second AUTH, and audit the rest
kixelated Sep 29, 2026
95c01d1
quest(auth): JS fetchGroup stops when its grant is revoked
kixelated Sep 30, 2026
4da7654
feat(net): replace narrow with a symmetric auth::Handle::limit
kixelated Sep 30, 2026
ee8d254
chore(net): box the attached route
kixelated Sep 30, 2026
dbbe027
Merge remote-tracking branch 'origin/main' into quest/m1/auth/README
kixelated Sep 30, 2026
dc2b986
refactor(net): rename auth::Handle::limit to authorize
kixelated Sep 30, 2026
fc5692c
Merge pull request #4554 from moq-dev/claude/plan-auth-violations
kixelated Sep 30, 2026
c508eba
Merge remote-tracking branch 'origin/quest/m1/auth/README' into tmp-4550
kixelated Sep 30, 2026
a45fc39
Merge remote-tracking branch 'origin/quest/m1/auth/README' into quest…
kixelated Sep 30, 2026
75ca4c9
fix(net): adapt to the merged base
kixelated Sep 30, 2026
1475441
quest(auth): plan lite NOT_SUPPORTED and the 65,535-byte message cap …
kixelated Sep 30, 2026
a0ef1f1
Merge remote-tracking branch 'origin/quest/m1/auth/README' into tmp-4550
kixelated Sep 30, 2026
67e16cb
Merge pull request #4560 from moq-dev/quest/m1/auth/narrowing
kixelated Sep 30, 2026
9f1d6e5
Merge origin/quest/m1/auth/README into quest/m1/auth/error-codes
kixelated Sep 30, 2026
d2755ac
test(net): bound the JS grant-stream regression wait
kixelated Sep 30, 2026
6857be9
fix(net): settle grants with the error that ended the session
kixelated Sep 30, 2026
363367d
Merge pull request #4550 from moq-dev/quest/m1/auth/error-codes
kixelated Sep 30, 2026
707f813
Merge origin/main into quest/m1/auth/README
kixelated Sep 30, 2026
8ad7606
quest(m1/auth): move peer grants to the P2P line
kixelated Sep 30, 2026
51c19e2
Merge pull request #4590 from moq-dev/claude/auth-peer-grant-move
kixelated Sep 30, 2026
7650f6c
Merge origin/main into quest/m1/auth/README
kixelated Oct 5, 2026
1746893
Merge origin/main into quest/m1/auth/README
kixelated Oct 5, 2026
657ce49
quest(auth): request caps owns the lite message ceiling; FFI shape first
kixelated Oct 6, 2026
1d109a1
quest(auth): drop the processor link, deleted on main (#4946)
kixelated Oct 6, 2026
3c79ea0
feat(net)!: AUTH moves to moq-lite-07-wip (#5004)
kixelated Oct 7, 2026
ebcff39
Merge origin/main into quest/m1/auth/README
kixelated Oct 9, 2026
a5e5acb
refactor(net): group a session's handles so Session::new stays under …
kixelated Oct 9, 2026
724e34c
Merge remote-tracking branch 'origin/main' into auth-line-iterate
kixelated Oct 9, 2026
f3f0ef3
fix(net): gate standalone FETCH, lite announces, and TRACK_INFO on th…
kixelated Oct 9, 2026
a9f7e1b
Merge origin/main into quest/m1/auth/README
kixelated Oct 9, 2026
2fa39d4
fix(net): hold the grant across FETCH responses and TRACK_STATUS
kixelated Oct 9, 2026
6b7008d
Merge origin/main into quest/m1/auth/README
kixelated Oct 9, 2026
a710bd8
test(net): write the mid-fetch grant test's frame with main's textFrame
kixelated Oct 9, 2026
7493661
fix(net): cap the IETF AUTH_ERROR reason from its prefix
kixelated Oct 9, 2026
960118c
Merge remote-tracking branch 'origin/main' into claude/quest-iterate-…
kixelated Oct 9, 2026
86a10fe
fix(net): adapt the subscription grant arm to main's run_subscription…
kixelated Oct 9, 2026
5291e53
quest: hold every incoming request to the grant through a dispatcher-…
kixelated Oct 9, 2026
22738c6
fix(net): hold joining FETCH and TRACK_STATUS answers to the grant
kixelated Oct 9, 2026
7ad96d7
feat(net): declare the offered Setup extensions with setup::Extensions
kixelated Oct 9, 2026
f7c8570
feat(tokio): an extensions field on the dial and listen Config
kixelated Oct 9, 2026
8e98dea
feat(js/net): an extensions option on connect and accept
kixelated Oct 9, 2026
dcfb54d
feat(net): decode a request's AUTHORIZATION TOKEN by the SETUP rules
kixelated Oct 9, 2026
8af5625
feat(js): decode a request's AUTHORIZATION TOKEN by the SETUP rules
kixelated Oct 9, 2026
76d08ba
fix(js): reset a TRACK_INFO answer when the grant shrinks mid-write
kixelated Oct 9, 2026
5c136aa
Merge remote-tracking branch 'origin/main' into claude/quest-iterate-…
kixelated Oct 9, 2026
8016021
test(interop): give the C++ cells their per-cell token URL
kixelated Oct 9, 2026
c2c12d0
Merge remote-tracking branch 'origin/main' into claude/quest-iterate-…
kixelated Oct 9, 2026
90211f9
Merge remote-tracking branch 'origin/quest/m1/auth/extensions' into q…
kixelated Oct 9, 2026
13f7549
Merge remote-tracking branch 'origin/quest/m1/auth/request-token-deco…
kixelated Oct 9, 2026
7277e5d
feat(auth): a token on a request authorizes that request
kixelated Oct 9, 2026
1d34a0e
fix(net): answer a subscription's updates in arrival order
kixelated Oct 9, 2026
2737117
refactor(net): one admission helper and one renewal queue
kixelated Oct 9, 2026
536f27b
refactor(auth)!: a request token's scope is one Option
kixelated Oct 9, 2026
210742e
fix(auth)!: refuse a request token a moq-lite session cannot carry
kixelated Oct 9, 2026
e26a4e5
refactor(net): route a legacy update's answer by the ID the sender holds
kixelated Oct 9, 2026
311a73c
fix(net): end a subscription's failed update on its own path
kixelated Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions doc/.vitepress/drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,20 @@ describe("tables render as tables", () => {

const rendered = html(page as DraftPage);

// The Bidirectional Streams table: one header, six body rows, each
// The Bidirectional Streams table: one header, seven body rows, each
// separated by a kramdown rule in the source. Cells carry the source's
// alignment, so match around the style attribute.
const header = /<th[^>]*>Stream<\/th>/.exec(rendered);
expect(header).not.toBeNull();

for (const stream of ["Announce", "Subscribe", "Fetch", "Probe", "Goaway", "Track"]) {
for (const stream of ["Announce", "Subscribe", "Fetch", "Probe", "Goaway", "Track", "Auth"]) {
expect(rendered).toMatch(new RegExp(`<td[^>]*>${stream}</td>`));
}

// One header row plus six body rows, in a single table.
// One header row plus seven body rows, in a single table.
const start = rendered.lastIndexOf("<table", header?.index);
const rows = rendered.slice(start).split("</table>")[0];
expect(rows.match(/<tr>/g) ?? []).toHaveLength(7);
expect(rows.match(/<tr>/g) ?? []).toHaveLength(8);
});
});

Expand Down
23 changes: 19 additions & 4 deletions doc/bin/relay/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ nothing is admitted because the server was down. A grant that names nothing
refuses, and so does one already expired: expiry is exact, with no grace for
clock skew, so keep the auth server's clock in sync.

**Revalidate.** On the grant's cadence the relay asks again. A grant that no
longer covers what the session holds, or that changes `root`, `mounts`,
`peer`, or `upstream`, closes the session; it is not resized in place. A
changed `tier` keeps the session and moves its stats from then on. A 401 or
**Revalidate.** On the grant's cadence the relay asks again. A grant with the
same `root` and `mounts` resizes the live session in place, narrower or wider,
so a moderation decision lands on the session it targets and can be lifted the
same way: what falls outside a narrower grant resets with `Unauthorized`, the
broadcasts the session published outside it abort, and everything else keeps
flowing. A wider grant brings those paths back, up to what the session was
admitted with. A one-shot HTTP `/fetch` ends on a narrower grant instead. A
changed `root`, `mounts`, `peer`, or `upstream` closes the session. A changed
`tier` keeps the session and moves its stats from then on. A 401 or
403 closes it. Any other failure retries with backoff and the session lives
until `expires`, so an outage always has the bound the server chose. A grant
with `revalidate` must set `expires` for that reason.
Expand Down Expand Up @@ -108,6 +113,16 @@ TOKEN` option with Token Type 0. HMAC, RSA, ECDSA, and EdDSA keys all work. A
key can itself be **scoped** at generation (`--root`, `--publish`,
`--subscribe`), after which it can never sign a broader token.

### On a request

A moq-transport client may also put the `AUTHORIZATION TOKEN` on a SUBSCRIBE or
PUBLISH\_NAMESPACE, and renew it in place with an update. The session's
credential is checked first, so a request it covers is served whatever the
request carries. moq-relay does not verify request tokens yet: one on a request
the session's credential does not cover is refused `NOT_SUPPORTED`. An
application built on moq-net can verify them itself with
`auth::Handle::requests()`.

### Claims

| Claim | Meaning |
Expand Down
18 changes: 18 additions & 0 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ A subscriber watching under a root sees advertisements named relative to that
root. When several routes advertise one prefix, each reader sees the best route
its scope can use.

## Authorization

On moq-lite 07 (`moq-lite-07-wip`, opt-in) each side presents a token on its
own Auth stream and learns what it may publish and subscribe to: a union of
[path patterns](#path-patterns), delivered exactly as issued rather than widened
to a prefix. Right after setup both sides present the credential the connection
already carried (the URL token, a client certificate, or nothing), so a
publisher learns before anyone subscribes whether its broadcasts can reach the
peer. More tokens can be added without reconnecting; the session's scope is the
union of every open token's grant.

A subscription or fetch that loses access resets with the `UNAUTHORIZED` stream
code and the session stays up. A client that publishes outside its grant closes
the session with `UNAUTHORIZED`, naming the path. moq-transport carries the same
exchange on draft-17+ through the [MoQ Auth extension](/draft/moq-auth), limited
to namespace prefixes. Older versions have no grant; the URL token keeps working
everywhere.

## Subscriptions

A subscriber names a broadcast and track. Delivery starts at the oldest group
Expand Down
14 changes: 11 additions & 3 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ subset drops. What a peer actually observes against this implementation:
- **History is one group.** A `FETCH` returns one group from the cache, or on drafts 14 through 19 the saved prefix of the group a new subscription just joined. A range of groups is refused, as is a draft 20+ `LOCATION_FILTER` that ends at Largest Object rather than an absolute End Location. On draft 20 and later, `FETCH_OK`'s End Location is the requested end, capped at Largest Object: a range holding no objects gets an empty fetch stream, and only a start past Largest Object is refused, with `INVALID_RANGE`. A relay's copy with no upstream subscription can't know Largest Object short of the track's end, so it echoes the requested end instead. JavaScript publishing refuses every `FETCH`. Datagrams are never fetchable. A Rust reader that only fetches learns the track from `TRACK_STATUS` instead of subscribing, so a finished track stays fetchable; draft 17 still subscribes, since its `TRACK_STATUS` answer cannot say whether the track is timed.
- **Timing.** A track whose `SUBSCRIBE_OK` declares no `TIMESCALE` is untimed, as is every track on drafts 14 through 16. A Rust reader that never subscribed takes the units from `TRACK_STATUS` on draft 18 and later.
- **Strict SETUP and parameters.** Repeated unknown `SETUP` options, GREASE included, are accepted if well-formed; a repeated known option is rejected. On draft 17 and later, a `GROUP_ORDER` other than Ascending (1) or Descending (2) closes the session. Message parameters follow the negotiated draft's lists: drafts 14 and 15 ignore an unknown or misplaced parameter, draft 16 ignores one defined only for another message but closes the session on an unknown one, and draft 17 and later close it on both.
- **One credential per session**, carried in `SETUP` and forwarded to the [auth server](/bin/relay/auth#the-contract) unverified. A token attached to an individual request is ignored. An alias reference or a second token closes the session.
- **Refused, not fatal.** A legal request this stack does not serve is rejected on its own and the session stays up: `FORWARD=0`, range filters, `TRACK_STATUS` to a JavaScript publisher, `SUBSCRIBE_TRACKS`, and the fetch forms above. On draft 19 and later, and in Rust on drafts 14 through 16, a subscription update may change only priority; any other update ends that subscription.
- **One credential per session**, carried in `SETUP` and forwarded to the [auth server](/bin/relay/auth#the-contract) unverified. A second token closes the session. We advertise no token cache, so an alias reference closes it with `UNKNOWN_AUTH_TOKEN_ALIAS`, and a registration on a request with `AUTH_TOKEN_CACHE_OVERFLOW`. A token on a `SUBSCRIBE` or `PUBLISH_NAMESPACE` authorizes that one request when the session's grant does not cover it and the application verifies request tokens; an update renews it, and a refused renewal ends only that request. A token on any other request is decoded and ignored.
- **Refused, not fatal.** A legal request this stack does not serve is rejected on its own and the session stays up: `FORWARD=0`, range filters, `TRACK_STATUS` to a JavaScript publisher, `SUBSCRIBE_TRACKS`, and the fetch forms above. On draft 19 and later, and in Rust on drafts 14 through 16, a subscription update may change only priority or renew a request token; any other update ends that subscription.
- **Datagrams** are a single normal object at object 0, forwarded without renumbering. Anything else is dropped, and a malformed one closes the session. Rust and JavaScript both carry them on every draft.
- **Priority.** Higher is served first. The IETF default of 128 is this stack's 127, and a track that never sets one is 127.
- **Size.** An object extension block larger than 64 KiB ends that subgroup stream. The session stays up. This cap is ours, not the draft's.
Expand All @@ -39,16 +39,19 @@ Several project drafts extend the IETF wire without breaking it, since `SETUP`
ignores unknown parameters: [cluster](/draft/moq-cluster) routing hop lists,
[solicit](/draft/moq-solicit) to make announcements opt-in,
[hidden](/draft/moq-hidden) to keep `.`-named namespaces out of discovery,
[auth](/draft/moq-auth) to tell each peer what it may publish and subscribe to,
[active-count](/draft/moq-active-count) to count the `NAMESPACE` messages
before a `SUBSCRIBE_NAMESPACE` is caught up, and
[probe](/draft/moq-probe) for bandwidth estimation.
Both libraries offer solicit and auth unless a side's `extensions` setting
declines one, which connects it as a peer that does not speak that extension.
[moq-e2ee](/draft/moq-e2ee) is not a transport extension. It encrypts
application payloads, so relays still forward named tracks they cannot read.
See [Encryption](/concept/hang#encryption).

### Deliberate deviations

These three answers differ from the draft on purpose. They are the
These four answers differ from the draft on purpose. They are the
product's model, not bugs, and the relay does not change them.

- **One publisher per path.** A broadcast path names one piece of content, so a
Expand All @@ -72,6 +75,11 @@ product's model, not bugs, and the relay does not change them.
requires an established upstream subscription before `SUBSCRIBE_OK`. From
moq-lite 05 the track stream answers first, and a missing track is refused
before `SUBSCRIBE_OK`.
- **An unknown token alias closes the session.** No alias is ever registered,
so a `DELETE` or `USE_ALIAS` token names one that does not exist.
[Draft 21 §8.9](https://www.ietf.org/archive/id/draft-ietf-moq-transport-21.html#section-8.9)
rejects the message, but `UNKNOWN_AUTH_TOKEN_ALIAS` exists only as a session
termination code, so we close the session with it.

## MSF

Expand Down
1 change: 1 addition & 0 deletions doc/lib/js/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ broadcast.announce();
- **Timing is per track, with no default.** A track that declares a `timescale` carries a `timestamp` on every frame; one without is [untimed](/concept/moq-lite#subscriptions) and its frames carry none. Unlike Rust, omitting it means untimed. Nothing stamps a frame for you: the publisher passes the timestamp, and the `Timed` type (`{ value, at }`) carries one into the `@moq/json` and `@moq/flate` producers.
- **Subscriber staleness is `maxDelay`.** It is media time. Passing the old `maxAge` key throws a `TypeError` naming `maxDelay`. Publisher retention is the separate `Track.Info.maxAge`.
- **Hidden paths** stay out of discovery unless the announce request opts in. See [hidden broadcasts](/concept/moq-lite#hidden-broadcasts).
- **Authorization is in band.** On moq-lite 07 (offered only when `moq-lite-07-wip` is listed in `webtransport.protocols`; the WebSocket fallback cannot offer it) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `auth.grant` watches what the relay lets this side publish and subscribe to, and `auth.add(token)` presents another token without reconnecting. Publishing outside the grant closes the connection with `SessionCode.Unauthorized`, naming the path. On moq-lite, a subscription the grant stops covering resets with `StreamCode.Unauthorized` and the session stays up.
- **A graceful close waits.** `await connection.close()` withdraws announcements and gives finished tracks up to one second. `abort()` ends immediately.

Examples:
Expand Down
1 change: 1 addition & 0 deletions doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ What you use it for, beyond what the concept page already describes:
- **Publish only while someone is watching.** `demand()` on a track, group, or broadcast says whether a subscriber is attached, which is how capture and transcode skip work nobody asked for. Holding a broadcast consumer is not demand. Holding a track consumer is, so drop one you are not reading. A shared fetch stays up until its last reader leaves.
- **You drive the session, or `moq-tokio` does.** `connect` and `accept` return a session plus a driver that never reads the clock itself. `moq_net::time::run` polls it on tokio or in the browser. `moq-tokio` and `moq-wasm` hide that. A custom transport implements `moq_net::transport::poll`.
- **A session caps what its peer can hold.** `session::Limits` (via `Client::with_limits` and `Server::with_limits`) bounds announces and subscriptions per session; the defaults suit a relay mesh, so lower them for untrusted peers. Past either, the session closes with `TOO_MANY_REQUESTS`. On moq-transport drafts 14 to 16 they also size the request-ID window, which every request counts against, so very low limits can starve it. Peer-declared lengths are capped before they are buffered.
- **Authorization is in band.** On moq-lite 07 (`moq-lite-07-wip`, opt-in) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `session.auth()` presents tokens without reconnecting and watches the grant the peer sent back. A server verifies tokens itself by taking `handshake.auth().requests()` before `ok()`. `authorize(&grant)` narrows or widens a live session on any version: what falls outside resets with `Unauthorized` and the session stays up. A client that publishes outside its grant closes the session, naming the path.
- **A graceful close waits.** `session.close().await` withdraws announcements and gives finished tracks up to one second to deliver. `abort` ends immediately. IETF drafts 14 through 16 send withdrawals without waiting.

```bash
Expand Down
Loading