Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 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
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
6519fe7
Merge the final auth line head (squashed into main as #4039)
kixelated Oct 11, 2026
8c3ca3d
Merge the #4039 squash commit into quest/m1/auth/extensions
kixelated Oct 11, 2026
a3613c1
Merge remote-tracking branch 'origin/main' into merge-5106
kixelated Oct 11, 2026
c4d27d2
fix(net): adapt the extensions tests to main's Restart event and Config
kixelated Oct 11, 2026
4d59db5
fix(relay): offer the listen extensions on WebSocket sessions too
kixelated Oct 11, 2026
d54698a
docs(relay): document [listen.extensions]
kixelated Oct 11, 2026
b8050ba
docs(relay): document [connect.extensions]
kixelated Oct 11, 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
11 changes: 9 additions & 2 deletions doc/bin/relay/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ description: TOML reference for moq-relay

`moq-relay relay.toml`. Every key is also a CLI flag and environment variable
that joins the section and key: `listen.tls.cert` is `--listen-tls-cert` /
`MOQ_LISTEN_TLS_CERT`. The exception is `listen.bind`, spelled `--listen` /
`MOQ_LISTEN`. Precedence is CLI, then env, then file, then defaults.
`MOQ_LISTEN_TLS_CERT`. The exceptions: `listen.bind` is spelled `--listen` /
`MOQ_LISTEN`, and `listen.extensions` and `connect.extensions` are TOML only.
Precedence is CLI, then env, then file, then defaults.
`moq-relay --help` lists every flag.

## \[listen]
Expand All @@ -32,6 +33,10 @@ bind = "127.0.0.1:4444"
[listen.unix] # Plaintext qmux over a Unix socket, gated by peer credentials.
bind = "/run/moq/internal.sock"
allow.uid = [1001]

[listen.extensions] # moq-transport SETUP extensions to offer, each on by default.
auth = false # Decline MoQ Auth (draft 17+).
solicit = false # Decline MoQ Solicit, so peers may announce unasked.
```

`timeout` bounds the whole handshake, transport through MoQ SETUP, so a peer
Expand Down Expand Up @@ -163,6 +168,8 @@ tls.cert = "relay.pem" # Present a client certificate (mTLS to pee
tls.key = "relay.key"
goaway.redirect = "same-host" # How far to trust a draining peer's redirect URI.
goaway.handover = "10s" # Cap on how long the drained upstream keeps serving.
extensions.auth = false # Decline MoQ Auth on outbound moq-transport sessions.
extensions.solicit = false # Decline MoQ Solicit. Both default to on, as in [listen.extensions].
```

A draining upstream may name a replacement URI. `same-host` follows it only
Expand Down
3 changes: 3 additions & 0 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ ignores unknown parameters: [cluster](/draft/moq-cluster) routing hop lists,
[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 (draft 17 and later) unless a side's
`extensions` setting declines one, which connects it as a peer that does not
speak that extension. The pooled JavaScript `Connection` has no such setting yet.
[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).
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 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 @@ -70,6 +76,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 @@ -118,7 +125,12 @@ async function acceptAlpn(
version: Ietf.IetfVersion,
wiring: SessionProps,
): Promise<Established> {
const { control, early, solicit, hidden, cluster, auth } = await exchangeSetup(transport, version, "moq-lite-js");
const { control, early, solicit, hidden, cluster, auth } = await exchangeSetup(
transport,
version,
"moq-lite-js",
wiring.extensions,
);

return new Ietf.Connection({
...wiring,
Expand Down Expand Up @@ -164,7 +176,7 @@ async function acceptSetup(
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, Ietf.initialMaxRequestId(true));
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 @@ -224,7 +236,7 @@ async function acceptNegotiated(
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, Ietf.initialMaxRequestId(true));
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 { 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 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 @@ -310,7 +317,7 @@ async function negotiate(url: URL, session: WebTransport, wiring: SessionProps):
const params = new Ietf.SetupOptions();
params.setVarint(Ietf.SetupOption.MaxRequestId, Ietf.initialMaxRequestId(false));
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 @@ -367,7 +374,12 @@ async function handshakeAlpn(
version: Ietf.IetfVersion,
wiring: SessionProps,
): Promise<Established> {
const { control, early, solicit, hidden, cluster, auth } = await exchangeSetup(session, version, "moq-lite-js");
const { control, early, solicit, hidden, cluster, auth } = await exchangeSetup(
session,
version,
"moq-lite-js",
wiring.extensions,
);

return new Ietf.Connection({
...wiring,
Expand Down
105 changes: 105 additions & 0 deletions js/net/src/connection/extensions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { expect, spyOn, test } from "bun:test";
import { SessionCode } from "../error.ts";
import { AuthMessage, AuthOk } from "../ietf/auth.ts";
import { PublishNamespace } from "../ietf/publish_namespace.ts";
import { RequestOk } from "../ietf/request.ts";
import { ALPN, Version } from "../ietf/version.ts";
import { createMockTransportPair } from "../mock.ts";
import * as Path from "../path.ts";
import { Stream } from "../stream.ts";
import { accept } from "./accept.ts";
import { type Extensions, offered } from "./extensions.ts";
import { exchangeSetup } from "./handshake.ts";

const VERSION = Version.DRAFT_18;
const URL_ = new URL("https://example.com");

/** What each side learns from the other's SETUP when each offers `client` and `server`. */
async function handshake(client: Extensions, server: Extensions) {
const pair = createMockTransportPair(ALPN.DRAFT_18);
const [dialed, accepted] = await Promise.all([
exchangeSetup(pair.client, VERSION, "test", offered(client)),
exchangeSetup(pair.server, VERSION, "test", offered(server)),
]);
return { dialed, accepted };
}

/** Accept a draft-18 session offering `extensions`, against a peer offering every extension. */
async function acceptWith(extensions: Extensions) {
const pair = createMockTransportPair(ALPN.DRAFT_18);
const [peer, connection] = await Promise.all([
exchangeSetup(pair.client, VERSION, "test", offered()),
accept({ transport: pair.server, url: URL_, extensions }),
]);
return { pair, peer, connection };
}

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

test("a declined extension stays out of SETUP", async () => {
// Control: both sides offer both.
const all = await handshake({}, {});
expect(all.accepted.solicit).toBe(true);
expect(all.accepted.auth).toBe(true);
expect(all.dialed.auth).toBe(true);

const none = await handshake({ auth: false, solicit: false }, {});
expect(none.accepted.solicit).toBeUndefined();
expect(none.accepted.auth).toBe(false);
// Negotiated only when both offer it, so the declining side does not speak it either.
expect(none.dialed.auth).toBe(false);
expect(none.dialed.solicit).toBe(true);
});

test("an AUTH to a side that declined it closes the session", async () => {
// Control: offered by both, the connection credential is granted.
const offering = await acceptWith({});
const granted = await Stream.open(offering.pair.client, { version: VERSION });
await new AuthMessage(0n, new Uint8Array()).encode(granted.writer, VERSION);
expect(await granted.reader.u53()).toBe(AuthOk.id);
offering.connection.close();

const logged = spyOn(console, "error").mockImplementation(() => void 0);
try {
const declined = await acceptWith({ auth: false });
const refused = await Stream.open(declined.pair.client, { version: VERSION });
// The session closes on the message type alone, so the rest of the write may fail.
await new AuthMessage(0n, new Uint8Array()).encode(refused.writer, VERSION).catch(() => undefined);
const info = await declined.pair.client.closed;
expect(info.closeCode).toBe(SessionCode.ProtocolViolation);
} finally {
logged.mockRestore();
}
});

test("a side that declined solicit takes an unsolicited announce", async () => {
const announce = async (session: Awaited<ReturnType<typeof acceptWith>>) => {
const stream = await Stream.open(session.pair.client, { version: VERSION });
await stream.writer.u53(PublishNamespace.id);
await new PublishNamespace({
requestId: 0n,
trackNamespace: Path.from("room"),
cluster: { hops: [session.peer.cluster.self], cost: 0n },
}).encode(stream.writer, VERSION);
return stream;
};

const declined = await acceptWith({ solicit: false });
const taken = await announce(declined);
expect(await taken.reader.u53()).toBe(RequestOk.id);
declined.connection.close();

// Control: our SETUP declared it, so the peer that implements it broke it.
const logged = spyOn(console, "error").mockImplementation(() => void 0);
try {
const declaring = await acceptWith({});
await announce(declaring);
await declaring.pair.client.closed;
expect(logged).toHaveBeenCalled();
} finally {
logged.mockRestore();
}
});
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 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 only changes moq-transport sessions.
*/
export interface Extensions {
/** The MoQ Auth extension: tokens presented and granted on their own stream (draft-17+). */
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 each unset extension 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
Expand Up @@ -2,6 +2,7 @@ import { StreamError } from "../error.ts";
import { type Hop, randomHop } from "../hop.ts";
import * as Ietf from "../ietf/index.ts";
import { Reader, Stream, UnexpectedEnd, 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 @@ -12,16 +13,17 @@ import { Reader, Stream, UnexpectedEnd, Writer } from "../stream.ts";
* Returns the control stream, the uni streams that arrived before it (see
* {@link receiveSetup}), 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` declines
* them: 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;
early: Reader[];
Expand All @@ -33,9 +35,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 @@ -55,7 +56,8 @@ export async function exchangeSetup(
solicit: received.solicit,
hidden: received.hidden,
cluster: { self, peer: received.cluster },
auth: received.auth,
// 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";
3 changes: 3 additions & 0 deletions js/net/src/connection/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
type WebTransportProps as WebTransportPropsType,
} from "./connect.ts";
import type { Established as EstablishedType } from "./established.ts";
import type { Extensions as ExtensionsType } from "./extensions.ts";
import type { GoawayProps, Redirect as RedirectType } from "./goaway.ts";
import { Reload, type ReloadDelay, type ReloadStatus } from "./reload.ts";
import type { Probe as ProbeType, Stats as StatsType } from "./stats.ts";
Expand Down Expand Up @@ -372,6 +373,8 @@ export namespace Connection {
export type CertificateHash = CertificateHashType;
/** An established one-shot session. */
export type Established = EstablishedType;
/** The moq-transport extensions a one-shot session offers. */
export type Extensions = ExtensionsType;
}

/** Throw if `props` cannot be honored, rather than silently dropping them. */
Expand Down
Loading
Loading