Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
e199b67
quest(m1): claim auth/bindings
kixelated Oct 21, 2026
30acf7d
feat(moq-ffi): expose session auth grants and tokens
kixelated Oct 21, 2026
f5a4377
feat(py,go): wrap session auth
kixelated Oct 21, 2026
8418edb
feat(kt): alias session auth and follow its grant as a Flow
kixelated Oct 21, 2026
099b9fa
feat(dart): alias session auth and stream its grant
kixelated Oct 21, 2026
7368870
feat(swift): wrap session auth
kixelated Oct 21, 2026
f196d5b
test(interop): binding clients print and enforce their grant
kixelated Oct 21, 2026
c168cea
doc(lib): document session auth in every binding
kixelated Oct 21, 2026
4cf66d6
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 21, 2026
6625228
feat(obs): present a token edited mid-stream without reconnecting
kixelated Oct 21, 2026
1207c50
Merge remote-tracking branch 'origin/main' into quest/m1/auth/bindings
kixelated Oct 21, 2026
cb26bb3
quest: finish auth bindings
kixelated Oct 21, 2026
5b20496
test(dart,moq-ffi): drain the client before the server, and format
kixelated Oct 21, 2026
8e2cb10
Merge remote-tracking branch 'origin/main' into quest/m1/auth/bindings
kixelated Oct 21, 2026
d016fa6
fix(bindings): independent grant watchers, and refuse tokens nothing …
kixelated Oct 21, 2026
96fed41
chore(dart): regenerate bindings
kixelated Oct 21, 2026
32161b3
fix(tokio): a token the peer closes cleanly ends without an error
kixelated Oct 21, 2026
c8db840
fix(tokio,obs): refuse an unsendable token at its source, keep token …
kixelated Oct 21, 2026
1caf0ce
Merge remote-tracking branch 'origin/main' into quest/m1/auth/issuer
kixelated Oct 21, 2026
198665d
Merge remote-tracking branch 'origin/main' into quest/m1/auth/bindings
kixelated Oct 21, 2026
affca2b
feat(net): present the connection credential in SETUP
mriise Oct 21, 2026
c62185b
feat(tokio): configured tokens and credential refresh
mriise Oct 21, 2026
0e80c6b
feat(ffi): configured tokens; spec, docs, and interop for SETUP tokens
mriise Oct 21, 2026
8ae9049
feat(bindings): mirror configured tokens in every wrapper
mriise Oct 21, 2026
6bcbde8
feat(js): configured tokens in SETUP, the URL, and AUTH
mriise Oct 21, 2026
451abaf
fix(tokio): count a configured token a session cannot send as unsuppo…
mriise Oct 21, 2026
b81dd03
docs(ffi): migrate MoqClientConfig.tokens
mriise Oct 21, 2026
7dd7169
chore(dart): regenerate moq_ffi for configured tokens
mriise Oct 21, 2026
5b48b53
Merge #6109 (auth/issuer) under token-in-band
mriise 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
9d4a891
Merge #6109 (c0b800c7a) under token-in-band
mriise Oct 22, 2026
9425db1
Merge remote-tracking branch 'origin/main' into quest/m1/auth/token-i…
mriise Oct 22, 2026
c825cb4
Merge origin/main into quest/m1/auth/token-in-band
kixelated Oct 25, 2026
82db438
fix: lint and help-text fixes after merging main
kixelated Oct 25, 2026
4867952
fix(auth): hold configured tokens before the dial, and per fallback URL
kixelated Oct 25, 2026
040906d
Merge remote-tracking branch 'origin/main' into quest/m1/auth/token-i…
kixelated Oct 25, 2026
206cfdd
fix(auth): address review on docs and the lite-07 credential test
kixelated Oct 25, 2026
c3a4d36
Merge origin/main into quest/m1/auth/token-in-band
kixelated Oct 25, 2026
0056878
fix(net): record the tokens connect props in the js/net API report
kixelated Oct 25, 2026
f42958f
feat(c): moq_client_config.tokens
kixelated Oct 25, 2026
2a79fc4
fix(net): no intra-doc link to the async-only Client from Version
kixelated Oct 25, 2026
71ca4c5
fix(dart): regenerate the raw bindings with the pinned generator
kixelated Oct 25, 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
3 changes: 3 additions & 0 deletions dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import 'package:moq_ffi/moq_ffi.dart';
/// A MoQ client: configure the TLS/bind knobs, then connect to a relay.
typedef Client = MoqClient;

/// The credentials a [Client] presents, replaceable while it runs.
typedef Tokens = MoqTokens;

/// A live pub/sub session with a relay, exposing publish and consume origins.
typedef Session = MoqSession;

Expand Down
22 changes: 21 additions & 1 deletion dart/moq/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class AnnounceOptions {
/// the FFI's [MoqClientConfig].
///
/// Every field is optional; a null keeps the native default. A value the native
/// side cannot use fails [Moq.connect] with `MoqException.Config`.
/// side cannot use fails [Moq.connect] with `ConfigMoqException`.
final class ConnectOptions {
/// Local socket address to bind, e.g. `0.0.0.0:0`.
final String? bind;
Expand All @@ -34,6 +34,13 @@ final class ConnectOptions {
/// Null offers every supported version, which [versions] lists.
final List<String>? versions;

/// Credentials every dial presents. The first is the connection credential,
/// sent in SETUP and, while an offered version can't carry it there, the
/// URL's `?jwt=`; each other one widens the session's grant over in-band
/// AUTH. A URL that already carries `?jwt=` keeps it, and then every token
/// rides AUTH. Replace them before they expire through [Moq.tokens].
final List<String>? tokens;

/// How long one connect may take, dial and handshake together; zero waits forever.
final Duration? timeout;

Expand Down Expand Up @@ -69,6 +76,7 @@ final class ConnectOptions {
const ConnectOptions({
this.bind,
this.versions,
this.tokens,
this.timeout,
this.race,
this.resolutionDelay,
Expand All @@ -84,6 +92,7 @@ final class ConnectOptions {
MoqClientConfig get _ffi => MoqClientConfig(
bind: bind,
versions: versions ?? const [],
tokens: tokens ?? const [],
timeout: timeout,
race: race,
resolutionDelay: resolutionDelay,
Expand Down Expand Up @@ -242,6 +251,17 @@ final class Moq {
/// on every reconnect.
Auth auth() => session.auth();

/// The credentials every dial presents ([ConnectOptions.tokens]), to
/// replace while running.
///
/// [Tokens.set_] replaces them, such as before the current ones expire: the
/// next dial presents them, and a live session with in-band AUTH presents
/// them at once, withdrawing each replaced token once its replacements are
/// answered, even when one is refused. After an authorization refusal the
/// client redials only if they changed. An empty token throws
/// `ConfigMoqException`.
Tokens tokens() => _client.tokens();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/// Drain within one second, then stop the client.
///
/// Explicit `moq-lite-07-wip` waits for the peer to receive finished tails; older
Expand Down
2 changes: 1 addition & 1 deletion dart/moq/lib/src/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'version.dart';
/// Everything [Server.listen] can be told.
///
/// A value the native side cannot use fails [Server.listen] with
/// `MoqException.Config`.
/// `ConfigMoqException`.
final class ListenOptions {
/// Local socket address to listen on, e.g. `127.0.0.1:4443` or `[::]:443`.
final String bind;
Expand Down
145 changes: 143 additions & 2 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import "dart:ffi";
import "dart:io" show Platform, File, Directory;
import "dart:isolate";
import "dart:typed_data";

import "package:ffi/ffi.dart";

import "uniffi_runtime.dart";
export "uniffi_runtime.dart";

Expand Down Expand Up @@ -3409,6 +3407,7 @@ class FfiConverterMoqBackoff {
class MoqClientConfig {
final String? bind;
final List<String> versions;
final List<String> tokens;
final Duration? timeout;
final Duration? race;
final Duration? resolutionDelay;
Expand All @@ -3423,6 +3422,7 @@ class MoqClientConfig {
MoqClientConfig({
this.bind = null,
this.versions = const [],
this.tokens = const [],
this.timeout = null,
this.race = null,
this.resolutionDelay = null,
Expand Down Expand Up @@ -3454,6 +3454,11 @@ class FfiConverterMoqClientConfig {
);
final versions = versions_lifted.value;
new_offset += versions_lifted.bytesRead;
final tokens_lifted = FfiConverterSequenceString.read(
Uint8List.view(buf.buffer, new_offset),
);
final tokens = tokens_lifted.value;
new_offset += tokens_lifted.bytesRead;
final timeout_lifted = FfiConverterOptionalDuration.read(
Uint8List.view(buf.buffer, new_offset),
);
Expand Down Expand Up @@ -3513,6 +3518,7 @@ class FfiConverterMoqClientConfig {
MoqClientConfig(
bind: bind,
versions: versions,
tokens: tokens,
timeout: timeout,
race: race,
resolutionDelay: resolutionDelay,
Expand All @@ -3533,6 +3539,7 @@ class FfiConverterMoqClientConfig {
final total_length =
FfiConverterOptionalString.allocationSize(value.bind) +
FfiConverterSequenceString.allocationSize(value.versions) +
FfiConverterSequenceString.allocationSize(value.tokens) +
FfiConverterOptionalDuration.allocationSize(value.timeout) +
FfiConverterOptionalDuration.allocationSize(value.race) +
FfiConverterOptionalDuration.allocationSize(value.resolutionDelay) +
Expand Down Expand Up @@ -3560,6 +3567,10 @@ class FfiConverterMoqClientConfig {
value.versions,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterSequenceString.write(
value.tokens,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterOptionalDuration.write(
value.timeout,
Uint8List.view(buf.buffer, new_offset),
Expand Down Expand Up @@ -3610,6 +3621,7 @@ class FfiConverterMoqClientConfig {
static int allocationSize(MoqClientConfig value) {
return FfiConverterOptionalString.allocationSize(value.bind) +
FfiConverterSequenceString.allocationSize(value.versions) +
FfiConverterSequenceString.allocationSize(value.tokens) +
FfiConverterOptionalDuration.allocationSize(value.timeout) +
FfiConverterOptionalDuration.allocationSize(value.race) +
FfiConverterOptionalDuration.allocationSize(value.resolutionDelay) +
Expand Down Expand Up @@ -13260,6 +13272,7 @@ class FfiConverterMoqServer {
abstract class MoqClientInterface {
void cancel();
UniffiRustFuture<MoqSession> connect({required String url});
MoqTokens tokens();
}

final _MoqClientFinalizer = Finalizer<Pointer<Void>>((ptr) {
Expand Down Expand Up @@ -13323,6 +13336,17 @@ class MoqClient implements MoqClientInterface {
moqExceptionErrorHandler,
);
}

MoqTokens tokens() {
return rustCallWithLifter(
(status) => uniffi_moq_ffi_fn_method_moqclient_tokens(
uniffiClonePointer(),
status,
),
FfiConverterMoqTokens.lift,
null,
);
}
}

class FfiConverterMoqClient {
Expand Down Expand Up @@ -13546,6 +13570,78 @@ class FfiConverterMoqSession {
}
}

abstract class MoqTokensInterface {
void set_({required List<String> tokens});
}

final _MoqTokensFinalizer = Finalizer<Pointer<Void>>((ptr) {
rustCall((status) => uniffi_moq_ffi_fn_free_moqtokens(ptr, status));
});

class MoqTokens implements MoqTokensInterface {
late final Pointer<Void> _ptr;
bool _isDisposed = false;
MoqTokens._(this._ptr) {
_MoqTokensFinalizer.attach(this, _ptr, detach: this);
}
factory MoqTokens.lift(Pointer<Void> ptr) {
return MoqTokens._(ptr);
}
Pointer<Void> uniffiClonePointer() {
if (_isDisposed) {
throw StateError("Object has already been disposed");
}
return rustCall(
(status) => uniffi_moq_ffi_fn_clone_moqtokens(_ptr, status),
);
}

void dispose() {
if (_isDisposed) {
return;
}
_isDisposed = true;
_MoqTokensFinalizer.detach(this);
rustCall((status) => uniffi_moq_ffi_fn_free_moqtokens(_ptr, status));
}

void set_({required List<String> tokens}) {
return rustCall((status) {
uniffi_moq_ffi_fn_method_moqtokens_set(
uniffiClonePointer(),
FfiConverterSequenceString.lower(tokens),
status,
);
}, moqExceptionErrorHandler);
}
}

class FfiConverterMoqTokens {
static MoqTokens lift(Pointer<Void> ptr) {
return MoqTokens.lift(ptr);
}

static Pointer<Void> lower(MoqTokens value) {
return value.uniffiClonePointer();
}

static int allocationSize(MoqTokens value) {
return 8;
}

static LiftRetVal<MoqTokens> read(Uint8List buf) {
final handle = buf.buffer.asByteData(buf.offsetInBytes).getInt64(0);
final pointer = Pointer<Void>.fromAddress(handle);
return LiftRetVal(MoqTokens.lift(pointer), 8);
}

static int write(MoqTokens value, Uint8List buf) {
final handle = lower(value);
buf.buffer.asByteData(buf.offsetInBytes).setInt64(0, handle.address);
return 8;
}
}

abstract class MoqVideoConsumerInterface {
void cancel();
UniffiRustFuture<MoqVideoDecodedFrame?> next();
Expand Down Expand Up @@ -18800,6 +18896,14 @@ external Pointer<Void> uniffi_moq_ffi_fn_method_moqclient_connect(
RustBuffer url,
);

@Native<Pointer<Void> Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
external Pointer<Void> uniffi_moq_ffi_fn_method_moqclient_tokens(
Pointer<Void> ptr,
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Pointer<Void> Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
Expand Down Expand Up @@ -18900,6 +19004,31 @@ external RustBuffer uniffi_moq_ffi_fn_method_moqsession_stats(
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Pointer<Void> Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
external Pointer<Void> uniffi_moq_ffi_fn_clone_moqtokens(
Pointer<Void> handle,
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Void Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
external void uniffi_moq_ffi_fn_free_moqtokens(
Pointer<Void> handle,
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Void Function(Pointer<Void>, RustBuffer, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
external void uniffi_moq_ffi_fn_method_moqtokens_set(
Pointer<Void> ptr,
RustBuffer tokens,
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Pointer<Void> Function(Pointer<Void>, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
Expand Down Expand Up @@ -20012,6 +20141,9 @@ external int uniffi_moq_ffi_checksum_method_moqclient_cancel();
@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqclient_connect();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqclient_tokens();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqsession_auth();

Expand Down Expand Up @@ -20045,6 +20177,9 @@ external int uniffi_moq_ffi_checksum_method_moqsession_state_changed();
@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqsession_stats();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqtokens_set();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_method_moqvideoconsumer_cancel();

Expand Down Expand Up @@ -20744,6 +20879,9 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqclient_connect() != 5016) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqclient_tokens() != 18102) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqsession_auth() != 1680) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
Expand Down Expand Up @@ -20777,6 +20915,9 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqsession_stats() != 36396) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtokens_set() != 58423) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqvideoconsumer_cancel() != 21122) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
Expand Down
2 changes: 0 additions & 2 deletions dart/moq_ffi/lib/src/uniffi_runtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "dart:ffi";
import "dart:io" show Platform, File, Directory;
import "dart:isolate";
import "dart:typed_data";

import "package:ffi/ffi.dart";

class UniffiInternalError implements Exception {
Expand Down Expand Up @@ -251,7 +250,6 @@ mixin FfiConverterPrimitive<T> on FfiConverter<T, T> {
@override
T lower(T value) => value;
}

Uint8List createUint8ListFromInt(int value) {
int length = value.bitLength ~/ 8 + 1;
if (length != 4 && length != 8) {
Expand Down
9 changes: 6 additions & 3 deletions doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ moq <MoQ side> fetch <track> [options]
```

The **MoQ side** goes first and attaches the process to the network:
`--connect <url>` dials a relay (the path is the auth path, `?jwt=`
carries a token), and `--broadcast <name>` names the broadcast. A process can
`--connect <url>` dials a relay (the path is the auth path), `--connect-token`
presents a token (repeat it to present several, see
[Tokens](/bin/relay/auth#tokens)), and `--broadcast <name>` names the
broadcast. A `?jwt=` in the URL still works, and is how a token reaches a
relay that predates `--connect-token`. A process can
instead host sessions with `--listen`, or both at once, admitting clients the
way the relay does (see [Authentication](/bin/relay/auth)). Listening imports and
exports share the relay's shutdown behavior: SIGINT or SIGTERM sends accepted
Expand Down Expand Up @@ -74,7 +77,7 @@ ffmpeg -re -i video.mp4 -c copy -f mpegts -pes_payload_size 0 -muxdelay 0 - | \
moq --connect https://relay.example.com/anon --broadcast my-stream.hang export ts | ffplay -

# With a token
moq --connect "https://relay.example.com/rooms/1?jwt=$TOKEN" --broadcast alice.hang import ts
moq --connect https://relay.example.com/rooms/1 --connect-token "$TOKEN" --broadcast alice.hang import ts
```

## Import
Expand Down
Loading