diff --git a/.changes/jwt-room-config-claim-keys b/.changes/jwt-room-config-claim-keys new file mode 100644 index 000000000..0d165ce08 --- /dev/null +++ b/.changes/jwt-room-config-claim-keys @@ -0,0 +1 @@ +patch type="fixed" "`RoomConfiguration` and `RoomAgentDispatch` now also read the camelCase keys of the `roomConfig` claim (`agentName`, `emptyTimeout`, and so on), so `TokenSourceResponse.jwtPayload?.roomConfiguration` returns the values of a real LiveKit token instead of null fields" diff --git a/lib/src/token_source/room_configuration.dart b/lib/src/token_source/room_configuration.dart index 82a3898b1..e5cd80e1a 100644 --- a/lib/src/token_source/room_configuration.dart +++ b/lib/src/token_source/room_configuration.dart @@ -16,6 +16,14 @@ import 'package:json_annotation/json_annotation.dart'; part 'room_configuration.g.dart'; +// Token endpoints use snake_case keys, while LiveKit tokens write the +// `roomConfig` claim as protobuf JSON with camelCase keys. Read both. +Map _withSnakeCaseKeys(Map json, Map camelToSnake) => { + ...json, + for (final entry in camelToSnake.entries) + if (json[entry.value] == null && json[entry.key] != null) entry.value: json[entry.key], +}; + /// Configuration for dispatching an agent to a room. @JsonSerializable() class RoomAgentDispatch { @@ -35,7 +43,8 @@ class RoomAgentDispatch { this.deployment, }); - factory RoomAgentDispatch.fromJson(Map json) => _$RoomAgentDispatchFromJson(json); + factory RoomAgentDispatch.fromJson(Map json) => + _$RoomAgentDispatchFromJson(_withSnakeCaseKeys(json, const {'agentName': 'agent_name'})); Map toJson() => _$RoomAgentDispatchToJson(this); } @@ -91,6 +100,15 @@ class RoomConfiguration { this.agents, }); - factory RoomConfiguration.fromJson(Map json) => _$RoomConfigurationFromJson(json); + factory RoomConfiguration.fromJson(Map json) => _$RoomConfigurationFromJson( + _withSnakeCaseKeys(json, const { + 'emptyTimeout': 'empty_timeout', + 'departureTimeout': 'departure_timeout', + 'maxParticipants': 'max_participants', + 'minPlayoutDelay': 'min_playout_delay', + 'maxPlayoutDelay': 'max_playout_delay', + 'syncStreams': 'sync_streams', + }), + ); Map toJson() => _$RoomConfigurationToJson(this); } diff --git a/test/token/token_source_test.dart b/test/token/token_source_test.dart index fe8cd2dfb..83eb12cfd 100644 --- a/test/token/token_source_test.dart +++ b/test/token/token_source_test.dart @@ -219,6 +219,46 @@ void main() { expect(config.agents!.first.agentName, 'demo-agent'); expect(config.agents!.first.metadata, '{"foo":"bar"}'); }); + + test('parses room configuration from the camelCase keys of the roomConfig claim', () { + final token = _generateToken( + roomConfig: { + 'name': 'demo-room', + 'emptyTimeout': 300, + 'departureTimeout': 20, + 'maxParticipants': 10, + 'minPlayoutDelay': 100, + 'maxPlayoutDelay': 500, + 'syncStreams': true, + 'agents': [ + { + 'agentName': 'demo-agent', + 'metadata': '{"foo":"bar"}', + 'deployment': 'staging', + }, + ], + }, + ); + + final response = TokenSourceResponse( + serverUrl: 'https://test.livekit.io', + participantToken: token, + ); + + final config = response.jwtPayload?.roomConfiguration; + expect(config, isNotNull); + expect(config!.name, 'demo-room'); + expect(config.emptyTimeout, 300); + expect(config.departureTimeout, 20); + expect(config.maxParticipants, 10); + expect(config.minPlayoutDelay, 100); + expect(config.maxPlayoutDelay, 500); + expect(config.syncStreams, isTrue); + expect(config.agents, hasLength(1)); + expect(config.agents!.first.agentName, 'demo-agent'); + expect(config.agents!.first.metadata, '{"foo":"bar"}'); + expect(config.agents!.first.deployment, 'staging'); + }); }); group('TokenSourceResponse', () {