Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9c28c39
quest: claim m1/auth/issuer
kixelated Oct 20, 2026
793b60e
quest: fold the one-active-session decision for m1/auth/issuer
kixelated Oct 20, 2026
d0cf12c
feat(moq-net): an auth handle that outlives its sessions
kixelated Oct 20, 2026
74995f3
wip(moq-tokio): build every connection session on one moq-net auth ha…
kixelated Oct 20, 2026
549ee5d
fix(auth): finish the moq-tokio half of the shared auth handle
kixelated Oct 20, 2026
1089bbc
feat(js/net): an auth handle that outlives its session
kixelated Oct 21, 2026
03d536c
fix(auth): keep the session's auth side out of the public d.ts
kixelated Oct 21, 2026
52729c2
quest: finish m1/auth/issuer
kixelated Oct 21, 2026
88e31cd
fix(moq-net): build the auth link without the async feature
kixelated Oct 21, 2026
bd0f1c5
test(js/net): drive the GOAWAY token test with a manual clock
kixelated Oct 21, 2026
4cf66d6
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 21, 2026
1caf0ce
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 21, 2026
73bc832
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 21, 2026
3ed5932
fix(auth): a draining session takes no new tokens
kixelated Oct 21, 2026
fad76b1
fix(auth): a session that sent a GOAWAY presents no new AUTH stream
kixelated Oct 21, 2026
c6f331e
fix(auth): a draining session's late refusal keeps its successor's grant
kixelated Oct 21, 2026
92e2491
Merge remote-tracking branch 'origin/main' into pr-6109
kixelated Oct 21, 2026
0c7acdd
fix(auth): a draining verdict defers to the active session, and a ret…
kixelated Oct 22, 2026
c0b800c
fix(js/net): a GOING_AWAY refusal leaves a shared token for the succe…
kixelated Oct 22, 2026
372ae0b
Merge remote-tracking branch 'origin/main' into merge-6109
kixelated Oct 22, 2026
4a5350a
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 22, 2026
ffa3394
fix(net): await the manual clock and build auth tests without async
kixelated Oct 22, 2026
3286e29
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 22, 2026
36c103d
docs(net): say a followed redirect is presented the connection's tokens
kixelated Oct 22, 2026
c7d069e
fix(net): a lost retry stream does not end a token as unsupported
kixelated Oct 22, 2026
a4c2e3c
fix(ffi): keep a granted token whose session drops before add returns
kixelated Oct 22, 2026
4496e4a
fix(js/net): settle the setup wait when an auth session closes
kixelated Oct 22, 2026
081b756
docs(js/net): say a private connection's auth handle follows its URL
kixelated Oct 22, 2026
7d894b0
refactor(net)!: resolve auth add with how the peer took the token
kixelated Oct 22, 2026
587a5d1
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 22, 2026
ce63eb0
Merge remote-tracking branch 'origin/main' into iterate-6109
kixelated Oct 23, 2026
dfc997d
Merge origin/main into quest/m1/auth/issuer
kixelated Oct 23, 2026
c0e3034
fix(auth): a draining session never ends a token as a clean close
kixelated Oct 23, 2026
5d80484
Merge remote-tracking branch 'origin/main' into agent-merge-6109-b
kixelated Oct 23, 2026
d7e075e
chore(dart): regenerate the FFI checksums for the auth doc changes
kixelated Oct 24, 2026
e8114ef
Merge remote-tracking branch 'origin/main' into agent-merge-6109-b
kixelated Oct 24, 2026
b7ef99a
test(net): box the lost-retry test's transport after main's erasure
kixelated Oct 24, 2026
f984b0f
test(relay): an invalid attachment label is retried, not ended
kixelated Oct 24, 2026
f7d8393
Merge remote-tracking branch 'origin/main' into agent-merge-6109-b
kixelated Oct 24, 2026
cc4c139
style(relay): format the invalid attachment test
kixelated Oct 24, 2026
d53b13b
Merge remote-tracking branch 'origin/main' into agent-merge-6109-b
kixelated Oct 24, 2026
2f6b5dd
test(js/net): drive the ended-auth-handle reload test with a manual c…
kixelated Oct 24, 2026
705f4a1
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 24, 2026
a52d00b
test: restore manual clocks through Installed, and cover a token adde…
kixelated Oct 24, 2026
ecd85fd
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 24, 2026
f8f0df9
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 24, 2026
6fa7c3d
quest: drop the deleted issuer quest's link from net-exports
kixelated Oct 24, 2026
37fb3dc
test(relay): read how the uring relay took a token from its Answer
kixelated Oct 24, 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
4 changes: 2 additions & 2 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20212,13 +20212,13 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqauth_grant() != 14588) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqauth_grant_changed() != 5195) {
if (uniffi_moq_ffi_checksum_method_moqauth_grant_changed() != 38858) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqauthtoken_cancel() != 59324) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqauthtoken_closed() != 31020) {
if (uniffi_moq_ffi_checksum_method_moqauthtoken_closed() != 40912) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqauthtoken_grant() != 31654) {
Expand Down
8 changes: 4 additions & 4 deletions doc/bin/relay/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ key can itself be **scoped** at generation (`--root`, `--publish`,
On moq-lite-07-wip or moq-transport draft-17 and later with MoQ Auth negotiated,
a client can present another JWT without reconnecting. Present its replacement
before the current grant expires and keep the returned token handle alive.
The native `Connection::auth()` handle re-presents retained tokens after a
reconnect; the JavaScript session's `auth.add()` presents one for that session.
The auth handle a reconnecting connection owns (native `Connection::auth()`,
JavaScript `connection.auth`) presents its tokens again on every reconnect.
Older versions refuse in-band tokens.

Each accepted token adds its publish and subscribe patterns to the session's
Expand Down Expand Up @@ -176,8 +176,8 @@ one that times out while waiting is refused without being sent. A sent check
always runs to completion, so the connection's `end` stays its last request and
`--limit-token` counts stay exact. On moq-lite, an
auth server that cannot answer, by timeout or outage, refuses a token as a
transient error, and `Connection::auth()` presents it again on its reconnect
backoff. On moq-transport the refusal is final and the token ends. Retrying
transient error, and the client presents it again after a backoff of 1s
doubling to 30s. On moq-transport the refusal is final and the token ends. Retrying
never extends an accepted grant, so a session whose last grant expires during an
outage still closes.

Expand Down
6 changes: 4 additions & 2 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,10 @@ everywhere. On moq-transport an added token is kept even without the extension,
so an announcement can name it: it rides that `PUBLISH_NAMESPACE` as its
authorization token. Naming a fresh token renews the announcement in place with
`REQUEST_UPDATE` from draft 16, and re-announces it on drafts 14 and 15, so it
can outlive the token it started with. The token belongs to the session that added it, so after a
reconnect the application adds it again and re-announces. moq-lite has no
can outlive the token it started with. A token added to a reconnecting
connection's auth handle is presented on every session it makes, so the
announcement keeps it across a reconnect with no re-add or re-announce; one
added to a single session's handle ends with that session. moq-lite has no
per-request token, so there an announcement rides the session's scope.

## Subscriptions
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ subset drops. What a peer actually observes against this implementation:
- **Compression.** Rust peers on draft 17 and later exchange compressed tracks when both offer the [Flate extension](/draft/moq-flate). Older peers receive one shared inflated copy. A publisher that seals the compression declaration makes that fallback unavailable: an incompatible peer is refused, preserving the sealed metadata. JavaScript does not offer the IETF extension.
- **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. Other malformed control input closes the session with the draft's code too, such as a server `SETUP` carrying `PATH` or `AUTHORITY`, a request cut short by FIN, or a second `GOAWAY` on one stream; a first `GOAWAY` on a request stream is legal.
- **One `SETUP` credential**, forwarded to the [auth server](/bin/relay/auth#the-contract) unverified. A second `SETUP` token closes the session. We advertise no token cache, so an alias reference in `SETUP` closes it with `PROTOCOL_VIOLATION`, one on a request with `UNKNOWN_AUTH_TOKEN_ALIAS`, and a registration on a request with `AUTH_TOKEN_CACHE_OVERFLOW`. Where the Rust side authorizes the peer or verifies tokens, a token on a `SUBSCRIBE` or `PUBLISH_NAMESPACE` is verified like an in-band session token and authorizes that one request alone, even where that authorization already covers it; it never widens the session. It is refused with `NOT_SUPPORTED` when nothing verifies it, when it is empty, or when its Token Type is anything but 0 (out of band). A side that does neither, JavaScript included, ignores the token. Rust refuses a request carrying more than one distinct token with `NOT_SUPPORTED`, since it verifies one; JavaScript, which verifies none, ignores them all. A token-authorized `SUBSCRIBE` still stays within the grant the peer gave us. A token-authorized `PUBLISH_NAMESPACE` needs a grant covering everything beneath the namespace, and the tracks we subscribe under it rely on that announce instead of our limit, ending with it. An update renews the token, except on a drafts 14 and 15 `PUBLISH_NAMESPACE`, which has no update, and a refused renewal ends only that request. Rust and JavaScript send the token a route names (`Route::auth`) on its `PUBLISH_NAMESPACE`, including the one drafts 14 and 15 answer a solicited `SUBSCRIBE_NAMESPACE` with, and swapping it renews the request with `REQUEST_UPDATE` from draft 16, or ends and re-sends it before; the inline `NAMESPACE` that answers a solicited `SUBSCRIBE_NAMESPACE` from draft 16 carries no token. The token belongs to the session that added it, so a reconnect means adding it again and re-announcing. A token on any other request is decoded and grants nothing, and a request repeating one is refused with `MALFORMED_AUTH_TOKEN`. On draft 17 and later, the negotiated [MoQ Auth extension](/concept/moq-lite#authorization) accepts additional session tokens in band.
- **One `SETUP` credential**, forwarded to the [auth server](/bin/relay/auth#the-contract) unverified. A second `SETUP` token closes the session. We advertise no token cache, so an alias reference in `SETUP` closes it with `PROTOCOL_VIOLATION`, one on a request with `UNKNOWN_AUTH_TOKEN_ALIAS`, and a registration on a request with `AUTH_TOKEN_CACHE_OVERFLOW`. Where the Rust side authorizes the peer or verifies tokens, a token on a `SUBSCRIBE` or `PUBLISH_NAMESPACE` is verified like an in-band session token and authorizes that one request alone, even where that authorization already covers it; it never widens the session. It is refused with `NOT_SUPPORTED` when nothing verifies it, when it is empty, or when its Token Type is anything but 0 (out of band). A side that does neither, JavaScript included, ignores the token. Rust refuses a request carrying more than one distinct token with `NOT_SUPPORTED`, since it verifies one; JavaScript, which verifies none, ignores them all. A token-authorized `SUBSCRIBE` still stays within the grant the peer gave us. A token-authorized `PUBLISH_NAMESPACE` needs a grant covering everything beneath the namespace, and the tracks we subscribe under it rely on that announce instead of our limit, ending with it. An update renews the token, except on a drafts 14 and 15 `PUBLISH_NAMESPACE`, which has no update, and a refused renewal ends only that request. Rust and JavaScript send the token a route names (`Route::auth`) on its `PUBLISH_NAMESPACE`, including the one drafts 14 and 15 answer a solicited `SUBSCRIBE_NAMESPACE` with, and swapping it renews the request with `REQUEST_UPDATE` from draft 16, or ends and re-sends it before; the inline `NAMESPACE` that answers a solicited `SUBSCRIBE_NAMESPACE` from draft 16 carries no token. A token added to a reconnecting connection's auth handle rides every session it makes, so the announcement keeps it across a reconnect; one added to a single session's handle ends with that session. A token on any other request is decoded and grants nothing, and a request repeating one is refused with `MALFORMED_AUTH_TOKEN`. On draft 17 and later, the negotiated [MoQ Auth extension](/concept/moq-lite#authorization) accepts additional session tokens in band.
- **Pause delivery.** `FORWARD=0` on a `SUBSCRIBE` or its update drops the subscription, closing its data streams and holding nothing upstream, while the request and its slot stay. `FORWARD=1` subscribes again as a new `SUBSCRIBE` with the same filter would, so nothing carries across the pause: its `REQUEST_OK` names the live edge it joined at. A paused update may change only priority and max age.
- **Refused, not fatal.** A legal request this stack does not serve is rejected on its own and the session stays up: the fetch forms above get `NOT_SUPPORTED`. Range Filters get `INVALID_FILTER` on draft 19 and later because the advertised filter limit is zero. On draft 21 and later, namespaces whose first tuple field is exactly `.` or `.session` get `DOES_NOT_EXIST` before application lookup; other dot prefixes still reach the application. Subscription updates accept priority, negotiated subscriber max age, and delivery timeouts; Rust also renews request tokens and judges `NEW_GROUP_REQUEST` as a join (see dynamic groups below). Other supported-but-unimplemented preferences end that subscription; a `TRACK_PROPERTY_FILTER` there closes the session with `PROTOCOL_VIOLATION`, since it belongs only to `SUBSCRIBE_TRACKS`. On draft 18 and later a namespace update may change its prefix; suffixes after the acknowledgement are relative to that new prefix.
- **Objects and datagrams.** Only subgroup 0 is accepted: a stream for any other subgroup, whether its header gives the ID or takes it from the first Object ID, is stopped and the session stays up. Rust relays preserve Object IDs, gaps, object properties, FIRST\_OBJECT, and END\_OF\_GROUP status objects between moq-transport peers for subgroup 0, including across a moq-lite-07 hop between two relays that negotiate the [ietf](/draft/moq-ietf) extension. Datagrams retain their IDs, properties, priority, and status. Plain Rust readers and moq-lite peers see consecutive frames and normal datagram payloads; JavaScript supports only normal datagrams at Object ID 0. A malformed datagram closes the session; a relayed one the downstream draft cannot carry is dropped. A relayed Timestamp leaves in the units the downstream response declares, and is dropped where it declares none (drafts 14 through 16), as for a plain frame; an immutable one that would change or vanish is refused. On a timed track the frame holds each object's time, so a mutable Timestamp that only repeats the immutable one is not restated; a status object has no frame, so it keeps its Timestamp among its properties. Only property 0x10 is read as a Timestamp. Property 0x06 stays opaque and keeps its value regardless of timestamp units; it is dropped across the draft-19 boundary, or refused when immutable, because draft 19 assigns it to SUBGROUP\_DELIVERY\_TIMEOUT. From draft 15, a status object carrying properties is refused, and one relayed from draft 14 leaves without them. Draft 14 allows them, but an END\_OF\_TRACK carrying properties ends the subscription as unsupported, since a track end has nowhere to keep them. A `FETCH` carries the END\_OF\_GROUP object on drafts 14 and 15; later drafts have no FETCH status, so their response ends at the last frame. A group cannot resume across route changes by frame ordinal once Object IDs skip ahead of the ordinals on either route; recovery refuses it instead of splicing different objects.
Expand Down
4 changes: 2 additions & 2 deletions doc/lib/js/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ broadcast.announce();
- **Follow restarts.** `origin.request(path, { announced: true })` follows whichever publisher instance serves the path: its `active` changes to a fresh broadcast on a restart, clears on an end, and resolves again on the next start, so read it reactively and close the request when done. Instances are never stitched, so subscribe again to each new broadcast. Another route of the same epoch taking over (epochs travel only on `moq-lite-07-wip`) keeps `active` as is, and each track subscription resumes there from the first group it lacks. A refusal ends the request. Add `epoch` to pin one instance; a plain `request(path)` keeps its accepted instance, can resolve blindly, and ends once that instance stops serving. To list what is live, iterate `announced(scope)`.
- **Discovery follows what you watch.** A `consume` origin asks the relay for announcements only under the prefixes its readers watch with `announced(scope)` or `request(path, { announced: true })`, stops asking about a prefix 2s after nothing at or beneath it is watched, and asks again on reconnect. A plain `request(path)` asks nothing and subscribes blind, but once it resolves through a discovered route it watches its path until closed, so it outlives the listing that found it. A relay that refuses an announce request turns that session's announced requests blind too.
- **Connections share by URL and versions.** `new Connection({ url })` pools and reconnects with backoff, which the elements use. An ordered `versions` list, spelled like `moq-lite-06` as in Rust, applies to both WebTransport and WebSocket; equivalent lists share, while a different order or an omitted list gets its own pool entry. Supplying your own transport options or origin selects a private loop. Receive estimates are on demand: observe `connection.probe` reactively or subscribe while you need them; `peek()` alone opens no PROBE stream.
- **GOAWAY moves the session.** The connection dials the replacement at once while the old session keeps serving its groups, up to `goaway.handover` (default 10s, or the relay's deadline when that is sooner). New requests keep opening on the old session until the replacement's route outranks it, which is a restart without an epoch: an unpinned announced request follows the replacement, while a blind or epoch-pinned request stays on its accepted instance. With an epoch, which travels only when `moq-lite-07-wip` is offered, the replacement serves the same broadcast, so open subscriptions move across without missing a group. An empty GOAWAY redials the same URL. A redirect stays on the same host unless `goaway.redirect` is `"follow"`.
- **GOAWAY moves the session.** The connection dials the replacement at once while the old session keeps serving its groups, up to `goaway.handover` (default 10s, or the relay's deadline when that is sooner). New requests keep opening on the old session until the replacement's route outranks it, which is a restart without an epoch: an unpinned announced request follows the replacement, while a blind or epoch-pinned request stays on its accepted instance. With an epoch, which travels only when `moq-lite-07-wip` is offered, the replacement serves the same broadcast, so open subscriptions move across without missing a group. An empty GOAWAY redials the same URL. A redirect stays on the same host unless `goaway.redirect` is `"follow"`, which also presents the tokens on the connection's `auth` handle to the new host.
- **One send estimate per connection.** `Bandwidth.Allocator` divides it by track priority, max-min fair within a tier. An idle track claims nothing. Publishers reserve against it so their targets sum to the estimate.
- **List a broadcast's tracks.** `for await (const event of broadcast.tracks())` reports each track the broadcast lists (`added`, with its `Track.Info`), one `synced`, then `added` and `removed` as tracks come and go, until the broadcast ends; leaving the loop stops listing. Over a session it needs `moq-lite-07-wip`, syncs at once with what this side holds, and the peer's tracks follow as `added`. A track served only on request is never listed. See [discovery](/concept/moq-lite#discovery).
- **One track per name.** Concurrent subscriptions share one request and producer. `createTrack` on a name with a request still queued answers it. The application owns sequence numbers: a track that replaces an ended one, created or requested, throws on an append that would reuse a sequence the name already wrote, so it writes its first group with an explicit sequence. Only a new broadcast starts over.
- **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. Omitting it means untimed, as in Rust. 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 counts from a group's successor, by media time and by wall clock, whichever reaches it first. 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 (opt in with `versions: ["moq-lite-07-wip", "moq-lite-06"]`, listing every fallback you still want) 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 or track listing the grant stops covering resets with `StreamCode.Unauthorized` and the session stays up. On moq-transport, `auth.add(token)` keeps the token even without the extension, and a broadcast announced with the `Auth.Auth` it returns (`broadcast.announce({ auth })`) carries it on that announcement's `PUBLISH_NAMESPACE`; announcing again with another renews the request. An `Auth.Auth` belongs to the session that added it, so after a reconnect, add the token again and re-announce.
- **Authorization is in band.** On moq-lite 07 (opt in with `versions: ["moq-lite-07-wip", "moq-lite-06"]`, listing every fallback you still want) 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 or track listing the grant stops covering resets with `StreamCode.Unauthorized` and the session stays up. On moq-transport, `auth.add(token)` keeps the token even without the extension, and a broadcast announced with the `Auth.Auth` it returns (`broadcast.announce({ auth })`) carries it on that announcement's `PUBLISH_NAMESPACE`; announcing again with another renews the request. A `Connection`'s `auth` handle (a getter on a pooled connection, following its URL) outlives its sessions: a token added there is presented on every reconnect and keeps riding the announcements that name it, with no re-add or re-announce. A one-shot session's `auth` ends with it, unless `Connection.connect` was given a `new Auth.Handle()` as `auth`.
- **A graceful close waits.** `await connection.close()` withdraws announcements and drains for up to one second. Application delivery requires explicit `moq-lite-07-wip`; older Lite versions wait only for transport ACKs. IETF media streams are not drained. `abort()` ends immediately.

Examples:
Expand Down
Loading
Loading