From a05c3ed6b4e0afd4fc11bd7ab49caa2815a89870 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:20:37 +0800 Subject: [PATCH 1/7] impl 1 --- lib/src/agent/session.dart | 26 ++++++++++++++++ lib/src/agent/session_options.dart | 49 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/lib/src/agent/session.dart b/lib/src/agent/session.dart index deae32893..02e5d88eb 100644 --- a/lib/src/agent/session.dart +++ b/lib/src/agent/session.dart @@ -20,6 +20,8 @@ import 'package:uuid/uuid.dart'; import '../core/room.dart'; import '../core/room_preconnect.dart'; +import '../e2ee/key_provider.dart'; +import '../e2ee/options.dart'; import '../events.dart'; import '../logger.dart'; import '../managers/event.dart'; @@ -175,6 +177,12 @@ class Session extends DisposableChangeNotifier { EventsListener? _roomListener; Timer? _agentTimeoutTimer; + /// Enables or disables end-to-end encryption for the session. + /// + /// Requires that encryption was configured via [SessionOptions.encryption] + /// or that the [Room] was created with [E2EEOptions]. + Future setEncryptionEnabled(bool enabled) => room.setE2EEEnabled(enabled); + /// Starts the session by fetching credentials and connecting to the room. Future start() async { if (room.connectionState != ConnectionState.disconnected) { @@ -185,6 +193,18 @@ class Session extends DisposableChangeNotifier { _setError(null); _agentTimeoutTimer?.cancel(); + // Configure E2EE on the room before connecting if encryption options are set. + final encryption = _options.encryption; + if (encryption != null) { + final BaseKeyProvider keyProvider = switch (encryption.key) { + SharedKeyEncryption(:final sharedKey) => await _createSharedKeyProvider(sharedKey), + KeyProviderEncryption(:final keyProvider) => keyProvider, + }; + room.engine.roomOptions = room.engine.roomOptions.copyWith( + encryption: E2EEOptions(keyProvider: keyProvider), + ); + } + final Duration timeout = _options.agentConnectTimeout; Future connect() async { @@ -380,6 +400,12 @@ class Session extends DisposableChangeNotifier { _error = newError; notifyListeners(); } + + static Future _createSharedKeyProvider(String sharedKey) async { + final keyProvider = await BaseKeyProvider.create(); + await keyProvider.setSharedKey(sharedKey); + return keyProvider; + } } enum SessionErrorKind { diff --git a/lib/src/agent/session_options.dart b/lib/src/agent/session_options.dart index eb6d262c5..03ec989bf 100644 --- a/lib/src/agent/session_options.dart +++ b/lib/src/agent/session_options.dart @@ -13,6 +13,45 @@ // limitations under the License. import '../core/room.dart'; +import '../e2ee/key_provider.dart'; + +/// Encryption key configuration for a [Session]. +/// +/// Use one of the named constructors to specify either a shared passphrase +/// or a pre-configured [BaseKeyProvider]. +sealed class SessionEncryptionKey { + const SessionEncryptionKey(); + + /// Use a shared passphrase string. + /// + /// A [BaseKeyProvider] is created internally using the string as a shared + /// key (recommended for maximum compatibility across SDKs). + const factory SessionEncryptionKey.sharedKey(String key) = SharedKeyEncryption; + + /// Use a pre-configured [BaseKeyProvider] for custom key management. + const factory SessionEncryptionKey.keyProvider(BaseKeyProvider provider) = + KeyProviderEncryption; +} + +/// A shared passphrase used to derive encryption keys. +class SharedKeyEncryption extends SessionEncryptionKey { + final String sharedKey; + const SharedKeyEncryption(this.sharedKey); +} + +/// A pre-configured [BaseKeyProvider] instance. +class KeyProviderEncryption extends SessionEncryptionKey { + final BaseKeyProvider keyProvider; + const KeyProviderEncryption(this.keyProvider); +} + +/// Encryption configuration for a [Session]. +class SessionEncryptionOptions { + /// The encryption key — either a shared passphrase or a custom key provider. + final SessionEncryptionKey key; + + const SessionEncryptionOptions({required this.key}); +} /// Options for creating a [Session]. class SessionOptions { @@ -30,21 +69,31 @@ class SessionOptions { /// to a failed state. final Duration agentConnectTimeout; + /// Optional encryption configuration for end-to-end encryption. + /// + /// When provided, the session will configure E2EE on the room before + /// connecting. Use [Session.setEncryptionEnabled] to toggle encryption + /// after the session has started. + final SessionEncryptionOptions? encryption; + SessionOptions({ Room? room, this.preConnectAudio = true, this.agentConnectTimeout = const Duration(seconds: 20), + this.encryption, }) : room = room ?? Room(); SessionOptions copyWith({ Room? room, bool? preConnectAudio, Duration? agentConnectTimeout, + SessionEncryptionOptions? encryption, }) { return SessionOptions( room: room ?? this.room, preConnectAudio: preConnectAudio ?? this.preConnectAudio, agentConnectTimeout: agentConnectTimeout ?? this.agentConnectTimeout, + encryption: encryption ?? this.encryption, ); } } From 05546705e06779cd089569f2b2416e6fca56497b Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:16:19 +0800 Subject: [PATCH 2/7] fixes --- lib/src/agent/session.dart | 41 ++++++++++++++++++++---------- lib/src/agent/session_options.dart | 36 ++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/lib/src/agent/session.dart b/lib/src/agent/session.dart index 02e5d88eb..f619524c8 100644 --- a/lib/src/agent/session.dart +++ b/lib/src/agent/session.dart @@ -152,6 +152,7 @@ class Session extends DisposableChangeNotifier { final _TokenSourceConfiguration _tokenSourceConfiguration; final Agent _agent = Agent(); + bool _isStarting = false; Agent get agent => _agent; SessionError? get error => _error; @@ -185,26 +186,15 @@ class Session extends DisposableChangeNotifier { /// Starts the session by fetching credentials and connecting to the room. Future start() async { - if (room.connectionState != ConnectionState.disconnected) { + if (_isStarting || room.connectionState != ConnectionState.disconnected) { logger.info('Session.start() ignored: room already connecting or connected.'); return; } + _isStarting = true; _setError(null); _agentTimeoutTimer?.cancel(); - // Configure E2EE on the room before connecting if encryption options are set. - final encryption = _options.encryption; - if (encryption != null) { - final BaseKeyProvider keyProvider = switch (encryption.key) { - SharedKeyEncryption(:final sharedKey) => await _createSharedKeyProvider(sharedKey), - KeyProviderEncryption(:final keyProvider) => keyProvider, - }; - room.engine.roomOptions = room.engine.roomOptions.copyWith( - encryption: E2EEOptions(keyProvider: keyProvider), - ); - } - final Duration timeout = _options.agentConnectTimeout; Future connect() async { @@ -217,6 +207,29 @@ class Session extends DisposableChangeNotifier { } try { + // Configure E2EE on the room before connecting if encryption options + // are set. Skipped when a custom room was provided, since the user is + // responsible for configuring E2EE on the room directly. + final encryption = _options.encryption; + if (encryption != null && !_options.isRoomProvided) { + if (room.e2eeManager != null) { + // Restart: the E2EE manager already exists from a previous session. + // Re-enable in case it was toggled off, before connect publishes + // any tracks. + await room.setE2EEEnabled(true); + } else { + // First start: create the key provider and configure room options. + // The E2EE manager will be created during connect() with encryption + // enabled by default. + final BaseKeyProvider keyProvider = switch (encryption.key) { + SharedKeyEncryption(:final sharedKey) => await _createSharedKeyProvider(sharedKey), + KeyProviderEncryption(:final keyProvider) => keyProvider, + }; + room.engine.roomOptions = room.engine.roomOptions.copyWith( + encryption: E2EEOptions(keyProvider: keyProvider), + ); + } + } final bool dispatchesAgent; if (_options.preConnectAudio) { dispatchesAgent = await room.withPreConnectAudio( @@ -249,6 +262,8 @@ class Session extends DisposableChangeNotifier { _setError(SessionError.connection(error)); _setConnectionState(ConnectionState.disconnected); _agent.disconnected(); + } finally { + _isStarting = false; } } diff --git a/lib/src/agent/session_options.dart b/lib/src/agent/session_options.dart index 03ec989bf..d055989c9 100644 --- a/lib/src/agent/session_options.dart +++ b/lib/src/agent/session_options.dart @@ -14,6 +14,7 @@ import '../core/room.dart'; import '../e2ee/key_provider.dart'; +import '../logger.dart'; /// Encryption key configuration for a [Session]. /// @@ -29,8 +30,7 @@ sealed class SessionEncryptionKey { const factory SessionEncryptionKey.sharedKey(String key) = SharedKeyEncryption; /// Use a pre-configured [BaseKeyProvider] for custom key management. - const factory SessionEncryptionKey.keyProvider(BaseKeyProvider provider) = - KeyProviderEncryption; + const factory SessionEncryptionKey.keyProvider(BaseKeyProvider provider) = KeyProviderEncryption; } /// A shared passphrase used to derive encryption keys. @@ -47,10 +47,16 @@ class KeyProviderEncryption extends SessionEncryptionKey { /// Encryption configuration for a [Session]. class SessionEncryptionOptions { - /// The encryption key — either a shared passphrase or a custom key provider. + /// The encryption key, either a shared passphrase or a custom key provider. final SessionEncryptionKey key; const SessionEncryptionOptions({required this.key}); + + /// Creates encryption options with a shared passphrase string. + SessionEncryptionOptions.sharedKey(String key) : key = SharedKeyEncryption(key); + + /// Creates encryption options with a pre-configured [BaseKeyProvider]. + SessionEncryptionOptions.keyProvider(BaseKeyProvider provider) : key = KeyProviderEncryption(provider); } /// Options for creating a [Session]. @@ -58,6 +64,9 @@ class SessionOptions { /// The underlying [Room] used by the session. final Room room; + /// Whether a custom [Room] was explicitly provided. + final bool isRoomProvided; + /// Whether to enable audio pre-connect with [PreConnectAudioBuffer]. /// /// If enabled, the microphone is activated before connecting to the room. @@ -81,7 +90,23 @@ class SessionOptions { this.preConnectAudio = true, this.agentConnectTimeout = const Duration(seconds: 20), this.encryption, - }) : room = room ?? Room(); + }) : isRoomProvided = room != null, + room = room ?? Room() { + if (room != null && encryption != null) { + logger.warning( + 'Both room and encryption were provided to SessionOptions. ' + 'The encryption option will be ignored. Configure E2EE on the Room directly.', + ); + } + } + + SessionOptions._({ + required this.room, + required this.isRoomProvided, + required this.preConnectAudio, + required this.agentConnectTimeout, + this.encryption, + }); SessionOptions copyWith({ Room? room, @@ -89,8 +114,9 @@ class SessionOptions { Duration? agentConnectTimeout, SessionEncryptionOptions? encryption, }) { - return SessionOptions( + return SessionOptions._( room: room ?? this.room, + isRoomProvided: room != null ? true : isRoomProvided, preConnectAudio: preConnectAudio ?? this.preConnectAudio, agentConnectTimeout: agentConnectTimeout ?? this.agentConnectTimeout, encryption: encryption ?? this.encryption, From dd1aff829118395dd027abf69b9515b00aedcd0a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:12:26 +0800 Subject: [PATCH 3/7] fixes 2 --- lib/src/agent/session_options.dart | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/src/agent/session_options.dart b/lib/src/agent/session_options.dart index d055989c9..8d9f0f0af 100644 --- a/lib/src/agent/session_options.dart +++ b/lib/src/agent/session_options.dart @@ -14,7 +14,6 @@ import '../core/room.dart'; import '../e2ee/key_provider.dart'; -import '../logger.dart'; /// Encryption key configuration for a [Session]. /// @@ -92,12 +91,7 @@ class SessionOptions { this.encryption, }) : isRoomProvided = room != null, room = room ?? Room() { - if (room != null && encryption != null) { - logger.warning( - 'Both room and encryption were provided to SessionOptions. ' - 'The encryption option will be ignored. Configure E2EE on the Room directly.', - ); - } + _validateEncryptionConfiguration(); } SessionOptions._({ @@ -106,7 +100,9 @@ class SessionOptions { required this.preConnectAudio, required this.agentConnectTimeout, this.encryption, - }); + }) { + _validateEncryptionConfiguration(); + } SessionOptions copyWith({ Room? room, @@ -122,4 +118,14 @@ class SessionOptions { encryption: encryption ?? this.encryption, ); } + + void _validateEncryptionConfiguration() { + if (isRoomProvided && encryption != null) { + throw ArgumentError.value( + encryption, + 'encryption', + 'Cannot be provided when room is also provided. Configure E2EE on the Room directly.', + ); + } + } } From 3e30b0859dca3e7fca2f90e5ff1e7bd8564dd352 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:51:05 +0800 Subject: [PATCH 4/7] chore: add changelog entry for session E2EE options --- .changes/simplify-session-e2ee | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/simplify-session-e2ee diff --git a/.changes/simplify-session-e2ee b/.changes/simplify-session-e2ee new file mode 100644 index 000000000..d63c8d54c --- /dev/null +++ b/.changes/simplify-session-e2ee @@ -0,0 +1 @@ +minor type="added" "Simplify enabling E2EE with Session API" From 90ee852d1f2a9b3456e45beea83e67e0b84f3e6a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:37:14 +0800 Subject: [PATCH 5/7] simplify --- lib/src/agent/session.dart | 35 +--------- lib/src/agent/session_options.dart | 107 ++++++++--------------------- lib/src/e2ee/options.dart | 8 +++ 3 files changed, 37 insertions(+), 113 deletions(-) diff --git a/lib/src/agent/session.dart b/lib/src/agent/session.dart index f619524c8..74ff69b49 100644 --- a/lib/src/agent/session.dart +++ b/lib/src/agent/session.dart @@ -20,8 +20,6 @@ import 'package:uuid/uuid.dart'; import '../core/room.dart'; import '../core/room_preconnect.dart'; -import '../e2ee/key_provider.dart'; -import '../e2ee/options.dart'; import '../events.dart'; import '../logger.dart'; import '../managers/event.dart'; @@ -180,8 +178,8 @@ class Session extends DisposableChangeNotifier { /// Enables or disables end-to-end encryption for the session. /// - /// Requires that encryption was configured via [SessionOptions.encryption] - /// or that the [Room] was created with [E2EEOptions]. + /// Requires that encryption was configured via [SessionOptions] (by passing + /// `encryption:`) or that the [Room] was created with [E2EEOptions]. Future setEncryptionEnabled(bool enabled) => room.setE2EEEnabled(enabled); /// Starts the session by fetching credentials and connecting to the room. @@ -207,29 +205,6 @@ class Session extends DisposableChangeNotifier { } try { - // Configure E2EE on the room before connecting if encryption options - // are set. Skipped when a custom room was provided, since the user is - // responsible for configuring E2EE on the room directly. - final encryption = _options.encryption; - if (encryption != null && !_options.isRoomProvided) { - if (room.e2eeManager != null) { - // Restart: the E2EE manager already exists from a previous session. - // Re-enable in case it was toggled off, before connect publishes - // any tracks. - await room.setE2EEEnabled(true); - } else { - // First start: create the key provider and configure room options. - // The E2EE manager will be created during connect() with encryption - // enabled by default. - final BaseKeyProvider keyProvider = switch (encryption.key) { - SharedKeyEncryption(:final sharedKey) => await _createSharedKeyProvider(sharedKey), - KeyProviderEncryption(:final keyProvider) => keyProvider, - }; - room.engine.roomOptions = room.engine.roomOptions.copyWith( - encryption: E2EEOptions(keyProvider: keyProvider), - ); - } - } final bool dispatchesAgent; if (_options.preConnectAudio) { dispatchesAgent = await room.withPreConnectAudio( @@ -415,12 +390,6 @@ class Session extends DisposableChangeNotifier { _error = newError; notifyListeners(); } - - static Future _createSharedKeyProvider(String sharedKey) async { - final keyProvider = await BaseKeyProvider.create(); - await keyProvider.setSharedKey(sharedKey); - return keyProvider; - } } enum SessionErrorKind { diff --git a/lib/src/agent/session_options.dart b/lib/src/agent/session_options.dart index 8d9f0f0af..f912687c9 100644 --- a/lib/src/agent/session_options.dart +++ b/lib/src/agent/session_options.dart @@ -13,59 +13,18 @@ // limitations under the License. import '../core/room.dart'; -import '../e2ee/key_provider.dart'; - -/// Encryption key configuration for a [Session]. -/// -/// Use one of the named constructors to specify either a shared passphrase -/// or a pre-configured [BaseKeyProvider]. -sealed class SessionEncryptionKey { - const SessionEncryptionKey(); - - /// Use a shared passphrase string. - /// - /// A [BaseKeyProvider] is created internally using the string as a shared - /// key (recommended for maximum compatibility across SDKs). - const factory SessionEncryptionKey.sharedKey(String key) = SharedKeyEncryption; - - /// Use a pre-configured [BaseKeyProvider] for custom key management. - const factory SessionEncryptionKey.keyProvider(BaseKeyProvider provider) = KeyProviderEncryption; -} - -/// A shared passphrase used to derive encryption keys. -class SharedKeyEncryption extends SessionEncryptionKey { - final String sharedKey; - const SharedKeyEncryption(this.sharedKey); -} - -/// A pre-configured [BaseKeyProvider] instance. -class KeyProviderEncryption extends SessionEncryptionKey { - final BaseKeyProvider keyProvider; - const KeyProviderEncryption(this.keyProvider); -} - -/// Encryption configuration for a [Session]. -class SessionEncryptionOptions { - /// The encryption key, either a shared passphrase or a custom key provider. - final SessionEncryptionKey key; - - const SessionEncryptionOptions({required this.key}); - - /// Creates encryption options with a shared passphrase string. - SessionEncryptionOptions.sharedKey(String key) : key = SharedKeyEncryption(key); - - /// Creates encryption options with a pre-configured [BaseKeyProvider]. - SessionEncryptionOptions.keyProvider(BaseKeyProvider provider) : key = KeyProviderEncryption(provider); -} +import '../e2ee/options.dart'; +import '../options.dart'; /// Options for creating a [Session]. class SessionOptions { /// The underlying [Room] used by the session. + /// + /// If neither [room] nor [encryption] is provided, a default [Room] is + /// created. Passing both throws [ArgumentError] — configure E2EE on the + /// [Room] directly if you need a custom [Room] with encryption. final Room room; - /// Whether a custom [Room] was explicitly provided. - final bool isRoomProvided; - /// Whether to enable audio pre-connect with [PreConnectAudioBuffer]. /// /// If enabled, the microphone is activated before connecting to the room. @@ -77,55 +36,43 @@ class SessionOptions { /// to a failed state. final Duration agentConnectTimeout; - /// Optional encryption configuration for end-to-end encryption. + /// Creates [SessionOptions]. /// - /// When provided, the session will configure E2EE on the room before - /// connecting. Use [Session.setEncryptionEnabled] to toggle encryption - /// after the session has started. - final SessionEncryptionOptions? encryption; - + /// Pass [encryption] to configure end-to-end encryption on the internally + /// created [Room]. Use [E2EEOptions.sharedKey] for the common shared-key + /// case. For advanced setups (custom [RoomOptions], per-participant keys), + /// build a [Room] yourself and pass it via [room] instead. + /// + /// Passing both [room] and [encryption] throws [ArgumentError]. SessionOptions({ Room? room, + E2EEOptions? encryption, this.preConnectAudio = true, this.agentConnectTimeout = const Duration(seconds: 20), - this.encryption, - }) : isRoomProvided = room != null, - room = room ?? Room() { - _validateEncryptionConfiguration(); - } + }) : room = _buildRoom(room, encryption); - SessionOptions._({ - required this.room, - required this.isRoomProvided, - required this.preConnectAudio, - required this.agentConnectTimeout, - this.encryption, - }) { - _validateEncryptionConfiguration(); + static Room _buildRoom(Room? room, E2EEOptions? encryption) { + if (room != null && encryption != null) { + throw ArgumentError( + 'SessionOptions: pass either `room` or `encryption`, not both. ' + 'To use encryption with a custom Room, configure E2EE on the Room directly.', + ); + } + if (encryption != null) { + return Room(roomOptions: RoomOptions(encryption: encryption)); + } + return room ?? Room(); } SessionOptions copyWith({ Room? room, bool? preConnectAudio, Duration? agentConnectTimeout, - SessionEncryptionOptions? encryption, }) { - return SessionOptions._( + return SessionOptions( room: room ?? this.room, - isRoomProvided: room != null ? true : isRoomProvided, preConnectAudio: preConnectAudio ?? this.preConnectAudio, agentConnectTimeout: agentConnectTimeout ?? this.agentConnectTimeout, - encryption: encryption ?? this.encryption, ); } - - void _validateEncryptionConfiguration() { - if (isRoomProvided && encryption != null) { - throw ArgumentError.value( - encryption, - 'encryption', - 'Cannot be provided when room is also provided. Configure E2EE on the Room directly.', - ); - } - } } diff --git a/lib/src/e2ee/options.dart b/lib/src/e2ee/options.dart index 4b26fecee..bfb1f01cb 100644 --- a/lib/src/e2ee/options.dart +++ b/lib/src/e2ee/options.dart @@ -24,4 +24,12 @@ class E2EEOptions { final BaseKeyProvider keyProvider; final EncryptionType encryptionType = EncryptionType.kGcm; const E2EEOptions({required this.keyProvider}); + + /// Creates [E2EEOptions] configured with a shared-key [BaseKeyProvider] + /// derived from the given passphrase. + static Future sharedKey(String key) async { + final keyProvider = await BaseKeyProvider.create(); + await keyProvider.setSharedKey(key); + return E2EEOptions(keyProvider: keyProvider); + } } From 5905bce0d743ace2a1d52fbe603b6e2d2bab078d Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 19 Apr 2026 03:03:26 +0800 Subject: [PATCH 6/7] Create fix-session-start-reentrance --- .changes/fix-session-start-reentrance | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/fix-session-start-reentrance diff --git a/.changes/fix-session-start-reentrance b/.changes/fix-session-start-reentrance new file mode 100644 index 000000000..286024ba9 --- /dev/null +++ b/.changes/fix-session-start-reentrance @@ -0,0 +1 @@ +patch type="fixed" "Guard Session.start() against concurrent calls" From dc7bd855887da093ae1034c62f35c0ada1a125a3 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 19 Apr 2026 03:10:49 +0800 Subject: [PATCH 7/7] comment --- lib/src/agent/session.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/agent/session.dart b/lib/src/agent/session.dart index 74ff69b49..43d44922c 100644 --- a/lib/src/agent/session.dart +++ b/lib/src/agent/session.dart @@ -180,6 +180,7 @@ class Session extends DisposableChangeNotifier { /// /// Requires that encryption was configured via [SessionOptions] (by passing /// `encryption:`) or that the [Room] was created with [E2EEOptions]. + /// Throws [LiveKitE2EEException] if encryption was not configured. Future setEncryptionEnabled(bool enabled) => room.setE2EEEnabled(enabled); /// Starts the session by fetching credentials and connecting to the room.