Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

🔄 Changed

- `StreamChatClient.updateSystemEnvironment` now sanitizes the passed `SystemEnvironment`: `sdkName`, `sdkVersion`, and `osName` are locked to internal defaults, and `sdkIdentifier` only accepts the `dart` → `flutter` promotion (other values, including a `flutter` → `dart` demotion, are ignored). `appName`, `appVersion`, `osVersion`, and `deviceModel` continue to pass through as-is.

## 10.1.0

✅ Added
Expand Down
26 changes: 19 additions & 7 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,33 @@ class StreamChatClient {

/// Updates the system environment information used by the client.
///
/// It allows you to set environment-specific information that will be
/// included in API requests, such as the application name, platform details,
/// and version information.
/// The passed [environment] is sanitized before being applied:
///
/// Overridable fields (passed through as-is):
/// - [SystemEnvironment.appName]
/// - [SystemEnvironment.appVersion]
/// - [SystemEnvironment.osVersion]
/// - [SystemEnvironment.deviceModel]
///
/// Immutable fields (custom values are ignored, internal defaults are
/// preserved):
/// - [SystemEnvironment.sdkName]
/// - [SystemEnvironment.sdkIdentifier]
/// - [SystemEnvironment.sdkVersion]
/// - [SystemEnvironment.osName]
///
/// Example:
/// ```dart
/// client.updateSystemEnvironment(
/// SystemEnvironment(
/// name: 'my_app',
/// version: '1.0.0',
/// sdkName: 'stream-chat',
/// sdkIdentifier: 'dart',
/// sdkVersion: StreamChatClient.packageVersion,
/// appName: 'my_app',
/// appVersion: '1.0.0',
/// ),
/// );
/// ```
///
/// See [SystemEnvironment] for more information on the available fields.
void updateSystemEnvironment(SystemEnvironment environment) {
_systemEnvironmentManager.updateEnvironment(environment);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ class SystemEnvironmentManager {
/// {@macro systemEnvironmentManager}
SystemEnvironmentManager({
SystemEnvironment? environment,
}) : _environment = switch (environment) {
final env? => env,
_ => SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'dart',
sdkVersion: PACKAGE_VERSION,
osName: CurrentPlatform.name,
),
};
}) : _environment = SystemEnvironment(
sdkName: _sdkName,
sdkIdentifier: _SdkIdentifier.dart,
sdkVersion: PACKAGE_VERSION,
osName: CurrentPlatform.name,
) {
if (environment != null) updateEnvironment(environment);
}

static const _sdkName = 'stream-chat';

/// Returns the Stream client user agent string based on the current
/// [environment] value.
Expand All @@ -31,7 +32,37 @@ class SystemEnvironmentManager {

/// Updates the current [SystemEnvironment].
void updateEnvironment(SystemEnvironment environment) {
_environment = environment;
_environment = _sanitize(environment);
}

/// Sanitizes the passed [SystemEnvironment]
/// Ignores custom values for:
/// - sdkName
/// - sdkVersion
/// - osName
/// Allows only the dart -> flutter promotion for:
/// - sdkIdentifier (any other value, including a flutter -> dart
/// demotion, is ignored)
/// Allows overriding of:
/// - appName
/// - appVersion
/// - osVersion
/// - deviceModel
SystemEnvironment _sanitize(SystemEnvironment environment) {
final incoming = _SdkIdentifier(environment.sdkIdentifier);
final current = _SdkIdentifier(_environment.sdkIdentifier);
final sdkIdentifier = incoming.precedence < current.precedence ? current : incoming;
final osName = _environment.osName;
return SystemEnvironment(
sdkName: _sdkName,
sdkIdentifier: sdkIdentifier,
sdkVersion: PACKAGE_VERSION,
appName: environment.appName,
appVersion: environment.appVersion,
osName: osName,
osVersion: environment.osVersion,
deviceModel: environment.deviceModel,
);
}
}

Expand Down Expand Up @@ -63,3 +94,18 @@ extension XStreamClientHeaderExtension on SystemEnvironment {
].nonNulls.join('|');
}
}

/// Known SDK identifiers ranked by precedence. A proposed update is only
/// accepted when its precedence is greater than or equal to the current
/// identifier's, which makes the dart -> flutter transition one-way and
/// causes unknown identifiers to fall back to the current value.
extension type const _SdkIdentifier(String value) implements String {
static const dart = _SdkIdentifier('dart');
static const flutter = _SdkIdentifier('flutter');

int get precedence => switch (this) {
dart => 0,
flutter => 1,
_ => -1,
};
}
2 changes: 1 addition & 1 deletion packages/stream_chat/lib/src/ws/websocket.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ class WebSocket with TimerHelper {
'api_key': apiKey,
'authorization': token.rawValue,
'stream-auth-type': token.authType.name,
if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent),
...queryParameters,
if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent),
};

final scheme = switch (baseUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,83 @@ void main() {
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
});

test('initializes with custom environment', () {
test('sanitizes environment passed to constructor', () {
const customEnv = SystemEnvironment(
sdkName: 'custom-sdk',
sdkIdentifier: 'custom',
sdkIdentifier: 'flutter',
sdkVersion: '1.0.0',
appName: 'test-app',
);

manager = SystemEnvironmentManager(environment: customEnv);

expect(manager.environment.sdkName, equals('custom-sdk'));
expect(manager.environment.sdkIdentifier, equals('custom'));
expect(manager.environment.sdkVersion, equals('1.0.0'));
// Immutable fields are forced to internal values.
expect(manager.environment.sdkName, equals('stream-chat'));
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
expect(manager.environment.osName, equals(CurrentPlatform.name));
// Whitelisted sdkIdentifier is accepted.
expect(manager.environment.sdkIdentifier, equals('flutter'));
// App fields are passed through.
expect(manager.environment.appName, equals('test-app'));
});

test('updates environment', () {
test('sanitizes environment on update', () {
const newEnv = SystemEnvironment(
sdkName: 'updated-sdk',
sdkIdentifier: 'updated',
sdkVersion: '2.0.0',
sdkName: 'stream-chat-android',
sdkIdentifier: 'android',
sdkVersion: '99.0.0',
appName: 'test-app',
appVersion: '2.0.0',
osName: 'spoofed-os',
osVersion: '14',
deviceModel: 'Pixel 7',
);

manager.updateEnvironment(newEnv);

expect(manager.environment.sdkName, equals('updated-sdk'));
expect(manager.environment.sdkIdentifier, equals('updated'));
expect(manager.environment.sdkVersion, equals('2.0.0'));
// Immutable fields are forced to internal values.
expect(manager.environment.sdkName, equals('stream-chat'));
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
expect(manager.environment.osName, equals(CurrentPlatform.name));
// Unknown sdkIdentifier falls back to the current value.
expect(manager.environment.sdkIdentifier, equals('dart'));
// App/device/os-version fields are passed through.
expect(manager.environment.appName, equals('test-app'));
expect(manager.environment.appVersion, equals('2.0.0'));
expect(manager.environment.osVersion, equals('14'));
expect(manager.environment.deviceModel, equals('Pixel 7'));
});

test('allows promoting sdkIdentifier from dart to flutter', () {
manager.updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'flutter',
sdkVersion: PACKAGE_VERSION,
),
);

expect(manager.environment.sdkIdentifier, equals('flutter'));
});

test('ignores demotion of sdkIdentifier from flutter to dart', () {
manager
..updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'flutter',
sdkVersion: PACKAGE_VERSION,
),
)
..updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'dart',
sdkVersion: PACKAGE_VERSION,
),
);

expect(manager.environment.sdkIdentifier, equals('flutter'));
});

test('userAgent returns proper header string', () {
Expand Down
Loading