Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0c50b27
feat(net): decode the AUTHORIZATION TOKEN structure on a request para…
ksletmoe-aws Sep 28, 2026
d4e4dc4
feat(net): authorize a SUBSCRIBE by a token on the request
ksletmoe-aws Sep 29, 2026
5e982b4
feat(net): carry the AUTHORIZATION TOKEN on REQUEST_UPDATE
ksletmoe-aws Sep 29, 2026
b216eda
feat(net): end a request-token subscription when its grant lapses or …
ksletmoe-aws Sep 29, 2026
a7c5329
feat(net): renew a request-token subscription from a REQUEST_UPDATE
ksletmoe-aws Sep 29, 2026
29fdcbb
feat(net): authorize a PUBLISH_NAMESPACE by a token on the request
ksletmoe-aws Sep 29, 2026
52673e0
feat(net): accept the AUTHORIZATION TOKEN on FETCH, PUBLISH, TRACK_ST…
ksletmoe-aws Sep 29, 2026
f4deb5a
feat(net): accept the AUTHORIZATION TOKEN message parameter in js/net
ksletmoe-aws Sep 29, 2026
a26abf6
docs(relay): note request-borne AUTHORIZATION TOKENs in auth.md
ksletmoe-aws Sep 29, 2026
e9a82d2
fix(net): close on an alias request token and race renewal against th…
ksletmoe-aws Sep 29, 2026
9a924d6
docs(relay): scope the request-token note to the moq-net seam (part B…
ksletmoe-aws Sep 29, 2026
695f7e3
fix(net): verify a request token without the MoQ Auth extension
ksletmoe-aws Sep 29, 2026
337f3e7
feat(net): let a client present a request token on its own requests
ksletmoe-aws Sep 29, 2026
39656a1
feat(tokio): expose Client::with_request_token on the moq-tokio wrapper
ksletmoe-aws Sep 30, 2026
751f939
fix(net): verify a request token when the union does not cover, not j…
ksletmoe-aws Sep 30, 2026
394a2c6
test(net): confirm the request token rides PUBLISH_NAMESPACE at every…
ksletmoe-aws Sep 30, 2026
808b601
feat(tokio): expose Request::auth() so a QUIC server can answer reque…
ksletmoe-aws Sep 30, 2026
6e67893
test(net): a pre-ok requests() consumer governs the session acceptor
ksletmoe-aws Sep 30, 2026
151c478
fix(net): a client presenting a request token bypasses dialing-side g…
ksletmoe-aws Sep 30, 2026
666293e
fix(net): wire the session auth handle into the legacy-draft subscrib…
ksletmoe-aws Sep 30, 2026
0bc57ac
feat(net): replace a client's request token on a live request
ksletmoe-aws Sep 30, 2026
b1e1ba4
fix(net): a token-bearing client authorizes requests outside its conn…
ksletmoe-aws Sep 30, 2026
04d7d58
feat(net): let a client decline the MoQ Auth extension
ksletmoe-aws Sep 30, 2026
95db740
feat(net): let a server decline the MoQ Solicit extension
ksletmoe-aws Sep 30, 2026
cb06ed1
style(net): apply rustfmt to the request-token changes
ksletmoe-aws Oct 1, 2026
0984b23
fix(net): adapt the request-token changes to the moved auth line
ksletmoe-aws Oct 1, 2026
aca5238
fix(net): keep the announce grant filter when the token cannot ride
ksletmoe-aws Oct 1, 2026
742b5f0
fix(net): keep request-token bytes out of message Debug output
ksletmoe-aws Oct 1, 2026
15a502e
fix(net): send a reprice without the unchanged request token
ksletmoe-aws Oct 1, 2026
b757d4c
fix(net): decide pre-draft-17 subscribe renewals silently
ksletmoe-aws Oct 1, 2026
bfa458c
fix(net): scope a request grant to the presenter and its request
ksletmoe-aws Oct 1, 2026
149efd4
fix(net): keep the local limit on the request-token path
ksletmoe-aws Oct 1, 2026
c116287
fix(net): keep a subscription's range when renewing its token
ksletmoe-aws Oct 2, 2026
e760e7a
fix(net): end a request whose stream closes during a renewal
ksletmoe-aws Oct 2, 2026
e7d93b7
feat(net): declare offered extensions with setup::Extensions
ksletmoe-aws Oct 2, 2026
22b951f
feat(net): present request tokens through the auth handle
ksletmoe-aws Oct 2, 2026
03e26c5
docs(quest): record the request-token review decisions
ksletmoe-aws Oct 2, 2026
c6c4530
test(net): state two request-token invariants explicitly
ksletmoe-aws Oct 2, 2026
d802ccc
fix(tokio): gate the client request token on a transport feature
ksletmoe-aws Oct 2, 2026
45a6385
fix(net): answer a pre-draft-17 renewal on draft-15 and draft-16
ksletmoe-aws Oct 2, 2026
64e2a8d
fix(net): refuse inbound AUTH when this endpoint declined the extension
ksletmoe-aws Oct 2, 2026
b86dd4b
fix(net): keep detecting a cancellation while a renewal is pending
ksletmoe-aws Oct 2, 2026
8c9a856
fix(net): apply cluster params and a token renewal on the same update
ksletmoe-aws Oct 2, 2026
a9883c4
style(docs): format auth.md with the repo markdown formatter
ksletmoe-aws Oct 2, 2026
f7dc815
fix(net): answer every buffered REQUEST_UPDATE while a verdict is pen…
ksletmoe-aws Oct 2, 2026
c51dd18
fix(net): route a pre-draft-17 renewal reply back to its subscription
ksletmoe-aws Oct 2, 2026
fe264a0
feat(net): advertise and enforce MAX_REQUEST_UPDATES
ksletmoe-aws Oct 2, 2026
c2b10b2
feat(net): record the peer's advertised MAX_REQUEST_UPDATES
ksletmoe-aws Oct 2, 2026
79d21c7
fix(net): keep one request-token renewal in flight per subscription
ksletmoe-aws Oct 2, 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
21 changes: 21 additions & 0 deletions doc/bin/relay/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ TOKEN` option with Token Type 0; `moq auth serve` verifies it the same way. HMAC
can itself be **scoped** at generation (`--root`, `--publish`, `--subscribe`),
after which it can never sign a broader token.

### On a request

The same `AUTHORIZATION TOKEN` may ride an individual request (SUBSCRIBE,
REQUEST\_UPDATE, PUBLISH\_NAMESPACE, FETCH, PUBLISH, SUBSCRIBE\_NAMESPACE,
TRACK\_STATUS), not only the SETUP. A request is authorized by the session's
grant first; when that does not cover the request's path, by the token on the
request; with neither it is refused `UNAUTHORIZED`. A request token's grant
covers only the request it rode on, never widens the session, and ends when
the request ends. A REQUEST\_UPDATE carrying a fresh token refreshes it, so a
long-lived request (an ingest PUBLISH\_NAMESPACE, a subscription) renews its
credential in place without reconnecting; a refused renewal leaves the old
grant standing until it lapses.

Verifying a request token is a moq-net library capability: an application takes
`auth::Handle::requests()` before running the session and answers each token
tagged with the request's path and kind (`auth::Request::path()`, `::kind()`).
moq-relay does not yet opt in on the request path, so as of this release it
refuses a request token with `NOT_SUPPORTED`; wiring a per-request lease into
the `--auth-url` server and the in-process [`admissions()`](#in-process) API is
a follow-up.

### Claims

| Claim | Meaning |
Expand Down
18 changes: 15 additions & 3 deletions js/net/src/connection/accept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Lite from "../lite/index.ts";
import type { Consumer as OriginConsumer, Producer as OriginProducer } from "../origin.ts";
import { Stream } from "../stream.ts";
import type { Established } from "./established.ts";
import * as Extensions from "./extensions.ts";
import { forwardAnnounced } from "./forward.ts";
import { exchangeSetup } from "./handshake.ts";

Expand Down Expand Up @@ -33,6 +34,9 @@ export interface AcceptProps {
* nothing. The entries retract when the session dies; see the `consume` connect option.
*/
consume?: OriginProducer;

/** The moq-transport extensions to offer; each is on unless set to `false`. */
extensions?: Extensions.Extensions;
}

/** The per-session wiring shared by every negotiated protocol path. */
Expand All @@ -41,6 +45,8 @@ type SessionProps = {
publish?: OriginConsumer;
/** Whether this side dialed; only the dialing side aborts on a publication its grant does not cover. */
client: boolean;
/** The moq-transport extensions this side offers. */
extensions: Extensions.Offered;
};

/**
Expand Down Expand Up @@ -69,6 +75,7 @@ async function acceptInner(
discovery: props.discovery ?? true,
publish: props.publish,
client: false,
extensions: Extensions.offered(props.extensions),
};

if (protocol === Ietf.ALPN.DRAFT_22) {
Expand Down Expand Up @@ -117,7 +124,12 @@ async function acceptAlpn(
version: Ietf.IetfVersion,
wiring: SessionProps,
): Promise<Established> {
const { control, solicit, hidden, cluster, auth } = await exchangeSetup(transport, version, "moq-lite-js");
const { control, solicit, hidden, cluster, auth } = await exchangeSetup(
transport,
version,
"moq-lite-js",
wiring.extensions,
);

return new Ietf.Connection({
...wiring,
Expand Down Expand Up @@ -162,7 +174,7 @@ async function acceptSetup(
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, 42069n);
params.setBytes(Ietf.SetupOption.Implementation, encoder.encode("moq-lite-js"));
Ietf.solicitIntoSetup(params);
Extensions.intoSetup(params, wiring.extensions, version);
Ietf.hiddenIntoSetup(params);

const server = new Ietf.ServerSetup({ version, parameters: params });
Expand Down Expand Up @@ -222,7 +234,7 @@ async function acceptNegotiated(
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, 42069n);
params.setBytes(Ietf.SetupOption.Implementation, encoder.encode("moq-lite-js"));
Ietf.solicitIntoSetup(params);
Extensions.intoSetup(params, wiring.extensions, setupVersion);
Ietf.hiddenIntoSetup(params);

const server = new Ietf.ServerSetup({ version: selectedVersion, parameters: params });
Expand Down
16 changes: 14 additions & 2 deletions js/net/src/connection/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as Hex from "../util/hex.ts";
import { dev, redact } from "../util/log.ts";
import { isWebTransportSupported } from "./browser.ts";
import type { Established } from "./established.ts";
import * as Extensions from "./extensions.ts";
import { forwardAnnounced } from "./forward.ts";
import { exchangeSetup } from "./handshake.ts";

Expand Down Expand Up @@ -110,6 +111,9 @@ export interface ConnectProps {
*/
consume?: OriginProducer;

/** The moq-transport extensions to offer; each is on unless set to `false`. */
extensions?: Extensions.Extensions;

/**
* Aborts the connection attempt with the signal's reason. An already-aborted
* signal rejects before anything opens, and aborting after the connection is
Expand All @@ -124,6 +128,8 @@ type SessionProps = {
publish?: OriginConsumer;
/** Whether this side dialed; only the dialing side aborts on a publication its grant does not cover. */
client: boolean;
/** The moq-transport extensions this side offers. */
extensions: Extensions.Offered;
};

// Save if WebSocket won the last race, so we won't give QUIC a head start next time.
Expand Down Expand Up @@ -170,6 +176,7 @@ async function connectInner(url: URL, props: Omit<ConnectProps, "url">, abort: P
discovery: props.discovery ?? true,
publish: props.publish,
client: true,
extensions: Extensions.offered(props.extensions),
};

if (props.transport) {
Expand Down Expand Up @@ -308,7 +315,7 @@ async function negotiate(url: URL, session: WebTransport, wiring: SessionProps):
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, 42069n);
params.setBytes(Ietf.SetupOption.Implementation, encoder.encode("moq-lite-js"));
Ietf.solicitIntoSetup(params);
Extensions.intoSetup(params, wiring.extensions, setupVersion);
Ietf.hiddenIntoSetup(params);

const client = new Ietf.ClientSetup({
Expand Down Expand Up @@ -365,7 +372,12 @@ async function handshakeAlpn(
version: Ietf.IetfVersion,
wiring: SessionProps,
): Promise<Established> {
const { control, solicit, hidden, cluster, auth } = await exchangeSetup(session, version, "moq-lite-js");
const { control, solicit, hidden, cluster, auth } = await exchangeSetup(
session,
version,
"moq-lite-js",
wiring.extensions,
);

return new Ietf.Connection({
...wiring,
Expand Down
20 changes: 20 additions & 0 deletions js/net/src/connection/extensions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from "bun:test";
import * as Ietf from "../ietf/index.ts";
import { intoSetup, offered } from "./extensions.ts";

test("every extension is offered by default", () => {
expect(offered()).toEqual({ auth: true, solicit: true });
expect(offered({ solicit: false })).toEqual({ auth: true, solicit: false });
});

test("only the offered extensions reach the SETUP", () => {
const all = new Ietf.SetupOptions();
intoSetup(all, offered(), Ietf.Version.DRAFT_18);
expect(Ietf.solicitFromSetup(all)).toBe(true);
expect(Ietf.Auth.fromSetup(all, Ietf.Version.DRAFT_18)).toBe(true);

const none = new Ietf.SetupOptions();
intoSetup(none, offered({ auth: false, solicit: false }), Ietf.Version.DRAFT_18);
expect(Ietf.solicitFromSetup(none)).toBeUndefined();
expect(Ietf.Auth.fromSetup(none, Ietf.Version.DRAFT_18)).not.toBe(true);
});
27 changes: 27 additions & 0 deletions js/net/src/connection/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Ietf from "../ietf/index.ts";

/**
* The moq-transport extensions a session offers in its SETUP. Each is on unless set to
* `false`; turning one off connects as a peer that does not speak it. moq-lite carries both
* in its core, so this applies to moq-transport sessions only.
*/
export interface Extensions {
/** The MoQ Auth extension: tokens presented and granted on their own stream. */
auth?: boolean;
/** The MoQ Solicit extension: the peer answers our SUBSCRIBE_NAMESPACE rather than announcing unasked. */
solicit?: boolean;
}

/** Every extension resolved to whether it is offered. */
export type Offered = Required<Extensions>;

/** Resolve unset extensions to offered. */
export function offered(extensions?: Extensions): Offered {
return { auth: extensions?.auth ?? true, solicit: extensions?.solicit ?? true };
}

/** Write the options for the offered extensions into a SETUP. */
export function intoSetup(params: Ietf.SetupOptions, extensions: Offered, version: Ietf.IetfVersion) {
if (extensions.solicit) Ietf.solicitIntoSetup(params);
if (extensions.auth) Ietf.Auth.intoSetup(params, version);
}
18 changes: 10 additions & 8 deletions js/net/src/connection/handshake.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Hop, randomHop } from "../hop.ts";
import * as Ietf from "../ietf/index.ts";
import { Reader, Stream, Writer } from "../stream.ts";
import * as Extensions from "./extensions.ts";

/**
* Draft-17+ SETUP exchange. Each side opens a uni stream, writes its Setup
Expand All @@ -10,16 +11,17 @@ import { Reader, Stream, Writer } from "../stream.ts";
*
* Returns the control stream plus what the peer's SETUP declared: whether it requires
* solicitation, which decides whether we announce namespaces unprompted (see the MoQ Solicit
* extension), its Hop ID (see the MoQ Cluster extension), and whether it offered MoQ Auth.
* We declare all three ourselves on
* every session: we send SUBSCRIBE_NAMESPACE for each prefix we want, so an unsolicited
* advertisement can tell us nothing we won't have asked for, and a peer that knows our Hop
* ID can withhold the advertisements that already flowed through us.
* extension), its Hop ID (see the MoQ Cluster extension), and whether MoQ Auth is negotiated.
* We declare our Hop ID on every session, and Solicit and Auth unless `extensions` turns them
* off: we send SUBSCRIBE_NAMESPACE for each prefix we want, so an unsolicited advertisement can
* tell us nothing we won't have asked for, and a peer that knows our Hop ID can withhold the
* advertisements that already flowed through us.
*/
export async function exchangeSetup(
transport: WebTransport,
version: Ietf.IetfVersion,
implementation: string,
extensions: Extensions.Offered,
): Promise<{
control: Stream;
solicit: boolean | undefined;
Expand All @@ -30,9 +32,8 @@ export async function exchangeSetup(
const encoder = new TextEncoder();
const params = new Ietf.SetupOptions();
params.setBytes(Ietf.SetupOption.Implementation, encoder.encode(implementation));
Ietf.solicitIntoSetup(params);
Extensions.intoSetup(params, extensions, version);
Ietf.hiddenIntoSetup(params);
Ietf.Auth.intoSetup(params, version);

// One id per session, like the moq-lite connection: nothing in this process forwards
// between sessions, so there is nothing for a shared id to detect.
Expand All @@ -51,7 +52,8 @@ export async function exchangeSetup(
solicit: received.solicit,
hidden: received.hidden,
cluster: { self, peer: received.cluster },
auth: received.auth,
// Auth is negotiated only when both sides offer it.
auth: received.auth && extensions.auth,
};
}

Expand Down
1 change: 1 addition & 0 deletions js/net/src/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
type WebTransportProps,
} from "./connect.ts";
export type { Established } from "./established.ts";
export type { Extensions } from "./extensions.ts";
export { Connection, type ConnectionProps } from "./pool.ts";
export type { Probe, Stats } from "./stats.ts";
export type { Transport } from "./transport.ts";
21 changes: 14 additions & 7 deletions js/net/src/ietf/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class Connection implements Established {

// What the peer declared about being solicited; see {@link Ietf.solicitFromSetup}.
#solicit: boolean | undefined;
/** Whether our SETUP declared MoQ Solicit, which is what makes an unasked announce a fault. */
#declaredSolicit: boolean;

// The Hop IDs this session declared; see {@link Cluster}.
#cluster?: Cluster.Hops;
Expand Down Expand Up @@ -103,6 +105,7 @@ export class Connection implements Established {
hidden = false,
cluster,
auth = false,
extensions,
}: {
url: URL;
quic: WebTransport;
Expand All @@ -126,8 +129,10 @@ export class Connection implements Established {
* cannot negotiate the extension, as is a `peer` the peer never declared.
*/
cluster?: Cluster.Hops;
/** Whether the peer's SETUP offered MoQ Auth (draft-17+). */
/** Whether MoQ Auth is negotiated (draft-17+): offered by both SETUPs. */
auth?: boolean;
/** What our own SETUP offered; every extension when omitted. */
extensions?: { solicit: boolean };
}) {
this.url = url;
this.discovery = discovery;
Expand Down Expand Up @@ -180,6 +185,7 @@ export class Connection implements Established {
ready: this.#auth.setupAnswered(),
});
this.#solicit = solicit;
this.#declaredSolicit = extensions?.solicit ?? true;
this.#cluster = cluster;
this.#subscriber = new Subscriber({ session: this.#session, quic, cluster, hidden, grant: this.#auth.grant });
registerWire(this, { consume: (path) => this.#subscriber.consume(path) });
Expand Down Expand Up @@ -300,18 +306,19 @@ export class Connection implements Established {
Cluster.negotiated(this.#cluster),
);

// We always declare that advertisements to us must be solicited (MoQ
// Solicit), and writing the option at all proves the peer implements the
// extension, whichever value it chose. It also cannot have advertised
// before reading our SETUP, since our SETUP is what says whether
// Unless told otherwise we declare that advertisements to us must be
// solicited (MoQ Solicit), and writing the option at all proves the peer
// implements the extension, whichever value it chose. It also cannot have
// advertised before reading our SETUP, since our SETUP is what says whether
// advertising unasked is allowed. So this is a bug in the peer, and a
// silent one on both sides if we tolerate it.
// silent one on both sides if we tolerate it. Without our declaration an
// unasked announce is what we invited.
//
// Draft-14/15 are exempt: they have no inline NAMESPACE, so a
// PUBLISH_NAMESPACE request is also how a peer answers our
// SUBSCRIBE_NAMESPACE there, and the message alone does not say which.
const legacy = this.#session.version === Version.DRAFT_14 || this.#session.version === Version.DRAFT_15;
if (this.#solicit !== undefined && !legacy) {
if (this.#declaredSolicit && this.#solicit !== undefined && !legacy) {
console.error(
`unsolicited publish_namespace from a peer that implements MoQ Solicit: broadcast=${msg.trackNamespace}`,
);
Expand Down
21 changes: 21 additions & 0 deletions js/net/src/ietf/ietf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ async function encodeFetchFrameVersioned(
return concatChunks(written);
}

test("message parameters accept and drop an AUTHORIZATION TOKEN (0x03)", async () => {
// A request-token peer may present the token on SUBSCRIBE, FETCH, PUBLISH, TRACK_STATUS,
// PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE. draft-16 tolerates unknown parameters
// generically; a draft-18 strict decoder must recognize 0x03 or it fails the whole
// message. We have no accept-side consumer, so the value is decoded and dropped.
const token = new Uint8Array([0x03, 0x81, 0x2c, 0x00, 0xff]);
for (const version of [Version.DRAFT_16, Version.DRAFT_18]) {
const params = new Parameters();
params.bytes.set(0x03n, token);

const { stream, written } = createTestWritableStream();
const writer = new Writer(stream, version);
await params.encode(writer, version);
writer.close();
await writer.closed;

const decoded = await Parameters.decode(new Reader(undefined, concatChunks(written), version), version);
expect(decoded.bytes.get(0x03n)).toEqual(token);
}
});

test("DEFAULT_PUBLISHER_PRIORITY has exact SUBSCRIBE_OK bytes per draft", async () => {
for (const version of [
Version.DRAFT_14,
Expand Down
6 changes: 6 additions & 0 deletions js/net/src/ietf/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ const MSG_PARAM_HIDDEN = 0x40b5en;

// Bytes parameter IDs (odd)
const MSG_PARAM_LARGEST_OBJECT = 0x09n;
/// AUTHORIZATION TOKEN (0x03): a per-request credential (MoQ request-token). This client has
/// no accept-side consumer to verify one, so it is decoded and dropped; an uncovered request
/// is then refused by the session grant as before. Recognizing it keeps a draft-17+ peer that
/// presents a token from failing the whole message on an unknown parameter.
const MSG_PARAM_AUTHORIZATION_TOKEN = 0x03n;
const MSG_PARAM_SUBSCRIPTION_FILTER = 0x21n;
/// FILL_PARAMETERS, draft-20's request for a backfill.
const MSG_PARAM_FILL_PARAMETERS = 0x23n;
Expand Down Expand Up @@ -245,6 +250,7 @@ function getMessageParamKind(id: bigint): MessageParamKind {
case MSG_PARAM_FILL_PARAMETERS:
case MSG_PARAM_INCLUDE_PROPERTIES:
case MSG_PARAM_HOP_PATH:
case MSG_PARAM_AUTHORIZATION_TOKEN:
return "bytes";
default:
throw new Error(`unknown message parameter id: ${id.toString()}`);
Expand Down
17 changes: 17 additions & 0 deletions quest/m1/auth/request-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ gives it meaning.
Public API: additive on `moq_net::auth::Request` (the request it belongs
to) and on `moq_auth::Client` (the per-request lease). Wire: none new; the parameter already exists in every supported draft.

Decided in review:

- Client credential: the request token rides the auth handle beside
session tokens, distinguished by kind (`auth::Handle::set_request_token`),
with no `Client` methods. `Connection::auth()` is not in the tree yet, so
moq-tokio seeds it from `connect::Config` on every (re)connected session;
live renewal there arrives with `Connection::auth()`, and is available on
moq-net's `Session::auth()` until then.
- Extensions: a positive `#[non_exhaustive] setup::Extensions { auth,
solicit }`, all on by default, on moq-net's client and server, moq-tokio's
dial and listen `Config`, and JS. Later extensions join it.
- Client-side live renewal stays in this quest: setting a new request token
on the handle re-presents it on live requests.
- `EXPIRED_AUTH_TOKEN` / `MALFORMED_AUTH_TOKEN` land with
[expired-error](/quest/m1/auth/expired-error.md); this quest answers
`UNAUTHORIZED` and `NOT_SUPPORTED`.

## Required

- [Relay tokens](/quest/m1/auth/relay-refresh.md) - supplies the lease
Expand Down
Loading
Loading