Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 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
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
9fe5478
Merge remote-tracking branch 'origin/main' into claude/quest-iterate-…
kixelated Oct 9, 2026
e54d5fc
fix(net): hold a lite announce restart to the subscribe limit
kixelated Oct 10, 2026
4265219
Merge remote-tracking branch 'origin/main' into claude/quest-iterate-…
kixelated Oct 10, 2026
3203667
fix(net): keep the announce cursor when the grant grows
kixelated Oct 10, 2026
8f04641
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
ce47554
quest: record the auth line landing in quest-flat-lines
kixelated Oct 10, 2026
8fdf05d
docs(js): name moq-lite-07-wip as the AUTH-capable lite version
kixelated Oct 10, 2026
9b99636
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
1388eab
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
872ebc2
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
0ab718d
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
5eb399f
fix(net): refuse trailing bytes in IETF AUTH replies
kixelated Oct 10, 2026
3c6c19a
Merge origin/main into quest/m1/auth/README
kixelated Oct 10, 2026
ab4f2c7
Merge the final auth line head (squashed into main as #4039)
kixelated Oct 11, 2026
22818a1
Merge the #4039 squash commit into quest/m1/auth/request-token-decode
kixelated Oct 11, 2026
df9cdc0
Merge remote-tracking branch 'origin/main' into merge-5107
kixelated Oct 11, 2026
05cc037
Merge remote-tracking branch 'origin/main' into merge-5107
kixelated Oct 11, 2026
2079d95
fix(net): give the request-token session test main's extensions field
kixelated Oct 11, 2026
7e1b07d
fix(net): close the session over a REQUEST_UPDATE's unusable token
kixelated Oct 12, 2026
f66ea2a
Merge remote-tracking branch 'origin/main' into merge-5107
kixelated Oct 12, 2026
f5818ed
fix(net): decode a token's whole structure, and keep SETUP aliases a …
kixelated Oct 12, 2026
32ae5d4
fix(net): refuse a request whose AUTHORIZATION TOKENs repeat
kixelated Oct 12, 2026
bf4c70b
fix(net): refuse repeated tokens through JS advertisement and legacy …
kixelated Oct 12, 2026
88232ac
docs(quest): name draft-16 namespace update tokens in the #5148 item
kixelated Oct 12, 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
9 changes: 7 additions & 2 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ 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.
- **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 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`. A token attached to an individual request is otherwise decoded and grants nothing, and a request repeating one is refused with `MALFORMED_AUTH_TOKEN`. `SUBSCRIBE_TRACKS` is refused unread, so its token is not decoded.
- **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.
- **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.
Expand All @@ -52,7 +52,7 @@ 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 @@ -76,6 +76,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 on a request 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
5 changes: 5 additions & 0 deletions drafts/draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,12 @@ Sent when terminating the session, via the transport's session close.
| ------- | ------------- | ----------- |
| 0x11 | CONTROL_MESSAGE_TIMEOUT | The peer took too long to respond to a control message. |
| ------- | ------------- | ----------- |
| 0x13 | AUTH_TOKEN_CACHE_OVERFLOW | A token registration would exceed the advertised token cache size. |
| ------- | ------------- | ----------- |
| 0x15 | VERSION_NEGOTIATION_FAILED | No version could be negotiated. |
| ------- | ------------- | ----------- |
| 0x17 | UNKNOWN_AUTH_TOKEN_ALIAS | A token named an alias that was never registered. |
| ------- | ------------- | ----------- |

### Stream Error Codes
Sent when resetting a stream (RESET_STREAM), or when refusing to receive one (STOP_SENDING).
Expand Down Expand Up @@ -1552,6 +1556,7 @@ The `Message Length` describes the payload size on the wire.
- Added announce compression: ANNOUNCE_START gains `Path Base` and `Path Keep` to copy the head of a live advertisement's suffix, and ANNOUNCE_START and ANNOUNCE_UPDATE gain `Hop Base` and `Hop Keep` to copy the tail of a live advertisement's Hop ID list.
- Capped the Message Length of every message except FRAME at 65,535 bytes.
- Added the TOO_MANY_REQUESTS (0x7) session code, closing a session whose peer goes past the endpoint's bound on subscriptions or announcements.
- Added the AUTH_TOKEN_CACHE_OVERFLOW (0x13) and UNKNOWN_AUTH_TOKEN_ALIAS (0x17) session codes, with moq-transport's meaning.
- Limited AUTH_OK Expires to 2^53 - 1 milliseconds, with an out-of-range value closing the session with PROTOCOL_VIOLATION.
- Invalid UTF-8 or an oversized AUTH_ERROR reason closes the session with PROTOCOL_VIOLATION.
- Added the Auth Stream (0x7) with AUTH, AUTH_OK, and AUTH_ERROR: either endpoint presents a token on its own stream and learns the [path patterns](#path-pattern) it may publish and subscribe to. The union of a session's open grants is its scope. A shrink withdraws what it no longer covers, and an announcement outside the scope closes the session with UNAUTHORIZED. A peer without the stream resets it.
Expand Down
2 changes: 2 additions & 0 deletions js/net/src/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ test("the code tables match the spec", () => {
expect(Number(SessionCode.Cancel)).toBe(0x0);
expect(Number(SessionCode.Unauthorized)).toBe(0x2);
expect(Number(SessionCode.GoawayTimeout)).toBe(0x10);
expect(Number(SessionCode.AuthTokenCacheOverflow)).toBe(0x13);
expect(Number(SessionCode.Version)).toBe(0x15);
expect(Number(SessionCode.UnknownAuthTokenAlias)).toBe(0x17);
expect(Number(StreamCode.Cancel)).toBe(0x1);
expect(Number(StreamCode.SessionClosed)).toBe(0x3);
expect(Number(StreamCode.TooFarBehind)).toBe(0x5);
Expand Down
10 changes: 9 additions & 1 deletion js/net/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ export const SessionCode = Object.freeze(
GoawayTimeout: 0x10 as SessionCode,
/** A control message took too long. */
Timeout: 0x11 as SessionCode,
/** A token registration would exceed the advertised token cache size, which is 0 when unadvertised. */
AuthTokenCacheOverflow: 0x13 as SessionCode,
/** No version could be negotiated. */
Version: 0x15 as SessionCode,
/** A token named an alias that was never registered. */
UnknownAuthTokenAlias: 0x17 as SessionCode,
} as const),
);

Expand Down Expand Up @@ -296,9 +300,13 @@ export class RefusedRedirect extends Error {
* @public
*/
export class ProtocolViolation extends Error {
constructor(message: string, options?: { cause?: unknown }) {
/** The session code to close with: `ProtocolViolation` unless the draft names a more specific one. */
readonly code: SessionCode;

constructor(message: string, options?: { cause?: unknown; code?: SessionCode }) {
super(message, options);
this.name = "ProtocolViolation";
this.code = options?.code ?? SessionCode.ProtocolViolation;
}
}

Expand Down
17 changes: 16 additions & 1 deletion js/net/src/ietf/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as Namespace from "./namespace.ts";
import { Parameters, SetupOption, SetupOptions } from "./parameters.ts";
import { PublishNamespace, PublishNamespaceUpdate } from "./publish_namespace.ts";
import { SubscribeNamespaceEntry } from "./subscribe_namespace.ts";
import { MalformedAuthToken } from "./token.ts";
import { type IetfVersion, Version } from "./version.ts";

const VERSION = Version.DRAFT_19;
Expand Down Expand Up @@ -241,7 +242,7 @@ test("Cluster: a negotiated PUBLISH_NAMESPACE may carry AUTHORIZATION_TOKEN", as
await wr.u62(1n);
await Namespace.encode(wr, Path.from("alice.hang"));
const params = Cluster.intoParams(advert);
params.bytes.set(0x03n, new Uint8Array([1, 2, 3])); // AUTHORIZATION_TOKEN
params.bytes.set(0x03n, new Uint8Array([0x03, 0x00, 0xaa])); // AUTHORIZATION_TOKEN: USE_VALUE, type 0
await params.encode(wr, version);
});
},
Expand All @@ -251,6 +252,20 @@ test("Cluster: a negotiated PUBLISH_NAMESPACE may carry AUTHORIZATION_TOKEN", as
expect(decoded.cluster).toEqual(advert);
});

test("Cluster: a repeated AUTHORIZATION_TOKEN refuses only the advertisement", async () => {
// USE_VALUE, type 0, value 0xAA, twice. A repeated key codes as a delta of 0.
const tokens = [2, 0x03, 3, 0x03, 0x00, 0xaa, 0, 3, 0x03, 0x00, 0xaa];
const frame = (body: number[]) => new Uint8Array([0, body.length, ...body]);

// Request ID 1 for namespace "a". The repeat is refused before the missing HOP_PATH is.
const advert = PublishNamespace.decode(reader(frame([1, 1, 1, 0x61, ...tokens])), VERSION, true);
await expect(advert).rejects.toThrow(MalformedAuthToken);
await expect(advert).rejects.toMatchObject({ requestId: 1n });

const update = PublishNamespaceUpdate.decode(reader(frame([1, ...tokens])), VERSION);
await expect(update).rejects.toThrow(MalformedAuthToken);
});

test("Cluster: every malformed PUBLISH_NAMESPACE update is a protocol violation", async () => {
const valid = await encode(new PublishNamespaceUpdate({ requestId: 1n, update: { cost: 2n } }));
const trailing = new Uint8Array(valid.byteLength + 1);
Expand Down
3 changes: 3 additions & 0 deletions js/net/src/ietf/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { type Hop, HopSchema, MAX_HOPS, UNKNOWN_HOP } from "../hop.ts";
import type { Reader } from "../stream.ts";
import * as Varint from "../varint.ts";
import { Parameters, SetupOption, type SetupOptions } from "./parameters.ts";
import { MalformedAuthToken } from "./token.ts";
import { type IetfVersion, Version } from "./version.ts";

/**
Expand Down Expand Up @@ -222,6 +223,8 @@ export async function decodeParams(
try {
params = await Parameters.decode(r, version, message);
} catch (err) {
// Keep a violation's own code, such as a request token's, and a refusal of the request alone.
if (err instanceof ProtocolViolation || err instanceof MalformedAuthToken) throw err;
throw new ProtocolViolation(reason(err), { cause: err });
}

Expand Down
Loading
Loading