From 25f34b51647cfb5fa4b261533257c46034d19742 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 10 Jul 2026 12:17:47 +0200 Subject: [PATCH 1/6] fix userId for guest --- .../api/interceptors/auth_interceptor.dart | 7 +- .../interceptors/auth_interceptor_test.dart | 95 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/stream_core/test/api/interceptors/auth_interceptor_test.dart diff --git a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart index ea7a0ed3..b56d7026 100644 --- a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart +++ b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart @@ -23,7 +23,12 @@ class AuthInterceptor extends QueuedInterceptor { try { final token = await _tokenManager.getToken(); - options.queryParameters['user_id'] = _tokenManager.userId; + // Use the resolved token's own user id rather than + // `_tokenManager.userId`: some token providers (e.g. guest exchanges) + // can resolve to a different id than the one originally requested, and + // this must stay consistent with the identity in the `Authorization` + // header below. + options.queryParameters['user_id'] = token.userId; options.headers['Authorization'] = token.rawValue; options.headers['stream-auth-type'] = token.authType.headerValue; diff --git a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart new file mode 100644 index 00000000..0337a76e --- /dev/null +++ b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; + +import 'package:stream_core/stream_core.dart'; +import 'package:test/test.dart'; + +// A minimal HttpClientAdapter that captures the outgoing RequestOptions and +// always responds with an empty successful response. +class _CapturingHttpClientAdapter implements HttpClientAdapter { + RequestOptions? lastRequest; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + lastRequest = options; + return ResponseBody.fromString( + '{}', + 200, + headers: { + Headers.contentTypeHeader: [Headers.jsonContentType], + }, + ); + } + + @override + void close({bool force = false}) {} +} + +UserToken _generateTestUserToken(String userId) { + String b64UrlNoPad(Object jsonObj) { + final bytes = utf8.encode(jsonEncode(jsonObj)); + return base64Url.encode(bytes).replaceAll('=', ''); + } + + final header = {'alg': 'none', 'typ': 'JWT'}; + final payload = {'user_id': userId}; + + final jwt = '${b64UrlNoPad(header)}.${b64UrlNoPad(payload)}.'; + return UserToken(jwt); +} + +void main() { + group('AuthInterceptor', () { + test( + "uses the resolved token's own user id for the user_id query " + 'parameter, not the id the TokenManager was constructed with', + () async { + // Simulates a token provider (e.g. a guest exchange) that resolves + // to a different id than the one originally requested. + final tokenManager = TokenManager( + userId: 'requested-id', + tokenProvider: TokenProvider.dynamic( + (_) async => _generateTestUserToken('server-assigned-id'), + ), + ); + + final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); + final adapter = _CapturingHttpClientAdapter(); + dio.httpClientAdapter = adapter; + dio.interceptors.add(AuthInterceptor(dio, tokenManager)); + + await dio.get('/test'); + + expect( + adapter.lastRequest?.queryParameters['user_id'], + 'server-assigned-id', + ); + }, + ); + + test( + 'matches the TokenManager userId when the token resolves to the ' + 'same id (regular/anonymous users)', + () async { + final tokenManager = TokenManager( + userId: 'user-123', + tokenProvider: TokenProvider.static( + _generateTestUserToken('user-123'), + ), + ); + + final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); + final adapter = _CapturingHttpClientAdapter(); + dio.httpClientAdapter = adapter; + dio.interceptors.add(AuthInterceptor(dio, tokenManager)); + + await dio.get('/test'); + + expect(adapter.lastRequest?.queryParameters['user_id'], 'user-123'); + }, + ); + }); +} From 234298a391522ce637b168d766755ddcee8bb5ba Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 10 Jul 2026 12:23:42 +0200 Subject: [PATCH 2/6] Add changelog entry --- packages/stream_core/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_core/CHANGELOG.md b/packages/stream_core/CHANGELOG.md index 7e039175..58f95ede 100644 --- a/packages/stream_core/CHANGELOG.md +++ b/packages/stream_core/CHANGELOG.md @@ -4,6 +4,10 @@ - Added `teams` field to `User` class. +### 🐛 Bug Fixes + +- `AuthInterceptor` now sets the `user_id` query parameter from the resolved token's own user id instead of `TokenManager.userId`. This keeps REST calls consistent for token providers that can resolve to a different id than requested (e.g. guest token exchanges). + ## 0.4.0 ### 💥 BREAKING CHANGES From 7ce30eb0ae92ac2388411d6651d9f8480a92dc0d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 11 Aug 2026 11:53:12 +0200 Subject: [PATCH 3/6] Update TokenManager authInterceptor --- packages/stream_core/CHANGELOG.md | 8 +- .../api/interceptors/auth_interceptor.dart | 38 +++++--- .../interceptors/auth_interceptor_test.dart | 91 ++++++++++++++++--- .../web_socket_connection_state_test.dart | 44 +++++++++ 4 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 packages/stream_core/test/ws/client/web_socket_connection_state_test.dart diff --git a/packages/stream_core/CHANGELOG.md b/packages/stream_core/CHANGELOG.md index 58f95ede..37f00237 100644 --- a/packages/stream_core/CHANGELOG.md +++ b/packages/stream_core/CHANGELOG.md @@ -1,12 +1,12 @@ ## Upcoming -### ✨ Features +### 💥 BREAKING CHANGES -- Added `teams` field to `User` class. +- `AuthInterceptor` now takes a `TokenManager Function()` getter instead of a `TokenManager` instance. This lets callers swap the active `TokenManager` at runtime — e.g. after a guest token exchange resolves a server-assigned user id — and have the interceptor pick up the new instance (and its `userId`) on the next request. -### 🐛 Bug Fixes +### ✨ Features -- `AuthInterceptor` now sets the `user_id` query parameter from the resolved token's own user id instead of `TokenManager.userId`. This keeps REST calls consistent for token providers that can resolve to a different id than requested (e.g. guest token exchanges). +- Added `teams` field to `User` class. ## 0.4.0 diff --git a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart index b56d7026..bd620066 100644 --- a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart +++ b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart @@ -4,16 +4,31 @@ import '../../errors.dart'; import '../../user.dart'; import '../stream_core_dio_error.dart'; +/// Provides the [TokenManager] currently in use by an [AuthInterceptor]. +/// +/// A getter rather than a fixed reference so the caller can swap the underlying +/// [TokenManager] at runtime — e.g. after a guest token exchange resolves a +/// server-assigned user id — and have the interceptor pick up the new instance. +typedef TokenManagerProvider = TokenManager Function(); + /// Authentication interceptor that refreshes the token if /// an auth error is received class AuthInterceptor extends QueuedInterceptor { - /// Initialize a new auth interceptor - AuthInterceptor(this._dio, this._tokenManager); + /// Initialize a new auth interceptor. + /// + /// [tokenManagerProvider] is a getter rather than a fixed reference so the + /// caller can swap the underlying [TokenManager] — e.g. after a guest token + /// exchange resolves a server-assigned user id — and have this interceptor + /// pick up the new instance on its next request. + AuthInterceptor(this._dio, this._tokenManagerProvider); final Dio _dio; - /// The token manager used in the client - final TokenManager _tokenManager; + /// Provides the token manager currently in use. + final TokenManagerProvider _tokenManagerProvider; + + /// The token manager currently in use. + TokenManager get _tokenManager => _tokenManagerProvider(); @override Future onRequest( @@ -23,12 +38,12 @@ class AuthInterceptor extends QueuedInterceptor { try { final token = await _tokenManager.getToken(); - // Use the resolved token's own user id rather than - // `_tokenManager.userId`: some token providers (e.g. guest exchanges) - // can resolve to a different id than the one originally requested, and - // this must stay consistent with the identity in the `Authorization` + // Re-read the token manager after awaiting the token: loading it may + // have swapped in a new manager carrying a server-resolved user id + // (e.g. a guest exchange). Reading `userId` here keeps the `user_id` + // query parameter consistent with the identity in the `Authorization` // header below. - options.queryParameters['user_id'] = token.userId; + options.queryParameters['user_id'] = _tokenManager.userId; options.headers['Authorization'] = token.rawValue; options.headers['stream-auth-type'] = token.authType.headerValue; @@ -62,10 +77,11 @@ class AuthInterceptor extends QueuedInterceptor { final error = StreamApiError.fromJson(data); if (error.isTokenExpiredError) { + final tokenManager = _tokenManager; // Don't try to refresh the token if we're using a static provider - if (_tokenManager.usesStaticProvider) return handler.next(err); + if (tokenManager.usesStaticProvider) return handler.next(err); // Otherwise, mark the current token as expired. - _tokenManager.expireToken(); + tokenManager.expireToken(); try { final options = err.requestOptions; diff --git a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart index 0337a76e..0082849a 100644 --- a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart +++ b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart @@ -28,6 +28,39 @@ class _CapturingHttpClientAdapter implements HttpClientAdapter { void close({bool force = false}) {} } +// An adapter that always responds with a token-expired API error (code 40), +// counting how many times it is hit so a retry can be detected. +class _TokenExpiredHttpClientAdapter implements HttpClientAdapter { + var _requestCount = 0; + int get requestCount => _requestCount; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + _requestCount++; + return ResponseBody.fromString( + jsonEncode({ + 'code': 40, // token expired + 'details': [], + 'duration': '0ms', + 'message': 'token expired', + 'more_info': '', + 'StatusCode': 401, + }), + 401, + headers: { + Headers.contentTypeHeader: [Headers.jsonContentType], + }, + ); + } + + @override + void close({bool force = false}) {} +} + UserToken _generateTestUserToken(String userId) { String b64UrlNoPad(Object jsonObj) { final bytes = utf8.encode(jsonEncode(jsonObj)); @@ -44,22 +77,30 @@ UserToken _generateTestUserToken(String userId) { void main() { group('AuthInterceptor', () { test( - "uses the resolved token's own user id for the user_id query " - 'parameter, not the id the TokenManager was constructed with', + 'picks up a TokenManager swapped in while the token is loading, so the ' + 'user_id query parameter reflects a server-resolved id (guest exchange)', () async { - // Simulates a token provider (e.g. a guest exchange) that resolves - // to a different id than the one originally requested. - final tokenManager = TokenManager( + // Simulates the guest flow: the token provider resolves to a + // server-assigned id and swaps in a new TokenManager carrying that id + // before the request headers are written. The interceptor reads the + // manager through the getter, so it observes the swapped instance. + late TokenManager tokenManager; + tokenManager = TokenManager( userId: 'requested-id', - tokenProvider: TokenProvider.dynamic( - (_) async => _generateTestUserToken('server-assigned-id'), - ), + tokenProvider: TokenProvider.dynamic((_) async { + final token = _generateTestUserToken('server-assigned-id'); + tokenManager = TokenManager( + userId: token.userId, + tokenProvider: TokenProvider.static(token), + ); + return token; + }), ); final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); final adapter = _CapturingHttpClientAdapter(); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, tokenManager)); + dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); await dio.get('/test'); @@ -71,8 +112,8 @@ void main() { ); test( - 'matches the TokenManager userId when the token resolves to the ' - 'same id (regular/anonymous users)', + 'uses the current TokenManager userId when nothing swaps it ' + '(regular/anonymous users)', () async { final tokenManager = TokenManager( userId: 'user-123', @@ -84,12 +125,38 @@ void main() { final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); final adapter = _CapturingHttpClientAdapter(); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, tokenManager)); + dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); await dio.get('/test'); expect(adapter.lastRequest?.queryParameters['user_id'], 'user-123'); }, ); + + test( + 'does not retry a token-expired response when using a static provider ' + '(e.g. a guest token): the error is surfaced to the caller instead of ' + 'silently re-minting the token', + () async { + final tokenManager = TokenManager( + userId: 'guest-1', + tokenProvider: TokenProvider.static(_generateTestUserToken('guest-1')), + ); + + final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); + final adapter = _TokenExpiredHttpClientAdapter(); + dio.httpClientAdapter = adapter; + dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + + await expectLater( + dio.get('/test'), + throwsA(isA()), + ); + + // A static provider must not trigger the refresh-and-retry path, so + // the request is attempted exactly once. + expect(adapter.requestCount, 1); + }, + ); }); } diff --git a/packages/stream_core/test/ws/client/web_socket_connection_state_test.dart b/packages/stream_core/test/ws/client/web_socket_connection_state_test.dart new file mode 100644 index 00000000..c8d9a6b9 --- /dev/null +++ b/packages/stream_core/test/ws/client/web_socket_connection_state_test.dart @@ -0,0 +1,44 @@ +import 'package:stream_core/stream_core.dart'; +import 'package:test/test.dart'; + +StreamApiError _apiError(int code) => StreamApiError( + code: code, + details: const [], + duration: '0ms', + message: 'error $code', + moreInfo: '', + statusCode: 401, +); + +Disconnected _serverDisconnect(StreamApiError apiError) => Disconnected( + source: ServerInitiated( + error: WebSocketEngineException( + reason: apiError.message, + code: 4001, + error: apiError, + ), + ), +); + +void main() { + group('WebSocketConnectionState.isAutomaticReconnectionEnabled', () { + test( + 'is disabled when the server closes with a token-expired error, so an ' + 'expired (e.g. guest) token does not trigger a silent reconnect loop', + () { + // Token-invalid error codes are 40..42; 40 = token expired. + final state = _serverDisconnect(_apiError(40)); + + expect(state.isAutomaticReconnectionEnabled, isFalse); + }, + ); + + test('is enabled for a generic, retryable server-initiated disconnection', () { + // A server error that is neither a normal closure (1000), a token error + // (40..42), nor a client error (400..499) should still reconnect. + final state = _serverDisconnect(_apiError(43)); + + expect(state.isAutomaticReconnectionEnabled, isTrue); + }); + }); +} From 5dc20d870a51725dbc5603689cb5470eed0142d0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 11 Aug 2026 13:05:14 +0200 Subject: [PATCH 4/6] Add test for retrying token --- .../interceptors/auth_interceptor_test.dart | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart index 0082849a..25340e83 100644 --- a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart +++ b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart @@ -29,8 +29,14 @@ class _CapturingHttpClientAdapter implements HttpClientAdapter { } // An adapter that always responds with a token-expired API error (code 40), -// counting how many times it is hit so a retry can be detected. +// counting how many times it is hit so a retry can be detected. [onFetch], if +// provided, runs when the request is dispatched — used to simulate a token +// manager being swapped in mid-flight. class _TokenExpiredHttpClientAdapter implements HttpClientAdapter { + _TokenExpiredHttpClientAdapter({this.onFetch}); + + final void Function()? onFetch; + var _requestCount = 0; int get requestCount => _requestCount; @@ -41,6 +47,7 @@ class _TokenExpiredHttpClientAdapter implements HttpClientAdapter { Future? cancelFuture, ) async { _requestCount++; + onFetch?.call(); return ResponseBody.fromString( jsonEncode({ 'code': 40, // token expired @@ -158,5 +165,46 @@ void main() { expect(adapter.requestCount, 1); }, ); + + test( + 'forwards a token-expired error without retrying when the token manager ' + 'is swapped to a static provider after the request was dispatched ' + '(guest exchange resolving mid-flight)', + () async { + // Starts on a dynamic manager and swaps to a static one carrying the + // server-resolved id once the request is already in flight, mirroring + // the guest flow. onError observes the swapped-in (static) manager and + // must forward the error rather than expire + retry. + var tokenManager = TokenManager( + userId: 'requested-id', + tokenProvider: TokenProvider.dynamic( + (_) async => _generateTestUserToken('requested-id'), + ), + ); + + final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); + final adapter = _TokenExpiredHttpClientAdapter( + onFetch: () { + tokenManager = TokenManager( + userId: 'server-assigned-id', + tokenProvider: TokenProvider.static( + _generateTestUserToken('server-assigned-id'), + ), + ); + }, + ); + dio.httpClientAdapter = adapter; + dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + + await expectLater( + dio.get('/test'), + throwsA(isA()), + ); + + // The swapped-in manager is static, so the error is surfaced without a + // refresh-and-retry: the request is attempted exactly once. + expect(adapter.requestCount, 1); + }, + ); }); } From 718e2730d2380db9a61575e006fe1a89dc3e5e2a Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 14 Aug 2026 12:20:09 +0200 Subject: [PATCH 5/6] keep auth_interceptor backwards compatible --- .../api/interceptors/auth_interceptor.dart | 40 ++++++++++++++----- .../interceptors/auth_interceptor_test.dart | 39 ++++++++++++++++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart index bd620066..f3719b34 100644 --- a/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart +++ b/packages/stream_core/lib/src/api/interceptors/auth_interceptor.dart @@ -14,21 +14,39 @@ typedef TokenManagerProvider = TokenManager Function(); /// Authentication interceptor that refreshes the token if /// an auth error is received class AuthInterceptor extends QueuedInterceptor { - /// Initialize a new auth interceptor. + /// Initialize a new auth interceptor backed by a fixed [tokenManager]. /// - /// [tokenManagerProvider] is a getter rather than a fixed reference so the - /// caller can swap the underlying [TokenManager] — e.g. after a guest token - /// exchange resolves a server-assigned user id — and have this interceptor - /// pick up the new instance on its next request. - AuthInterceptor(this._dio, this._tokenManagerProvider); + /// Use this when the [TokenManager] never changes for the lifetime of the + /// interceptor. If you need to swap the manager at runtime — e.g. after a + /// guest token exchange resolves a server-assigned user id — use + /// [AuthInterceptor.withProvider] instead. + AuthInterceptor( + this._dio, + TokenManager tokenManager, + ) : _tokenManager = tokenManager, + _tokenManagerProvider = null; + + /// Initialize a new auth interceptor backed by a [tokenManagerProvider]. + /// + /// The provider is a getter rather than a fixed reference so the caller can + /// swap the underlying [TokenManager] — e.g. after a guest token exchange + /// resolves a server-assigned user id — and have this interceptor pick up + /// the new instance on its next request. + AuthInterceptor.withProvider( + this._dio, { + required TokenManagerProvider tokenManagerProvider, + }) : _tokenManager = null, + _tokenManagerProvider = tokenManagerProvider; final Dio _dio; + final TokenManager? _tokenManager; + /// Provides the token manager currently in use. - final TokenManagerProvider _tokenManagerProvider; + final TokenManagerProvider? _tokenManagerProvider; /// The token manager currently in use. - TokenManager get _tokenManager => _tokenManagerProvider(); + TokenManager get _effectiveTokenManager => _tokenManager ?? _tokenManagerProvider!.call(); @override Future onRequest( @@ -36,14 +54,14 @@ class AuthInterceptor extends QueuedInterceptor { RequestInterceptorHandler handler, ) async { try { - final token = await _tokenManager.getToken(); + final token = await _effectiveTokenManager.getToken(); // Re-read the token manager after awaiting the token: loading it may // have swapped in a new manager carrying a server-resolved user id // (e.g. a guest exchange). Reading `userId` here keeps the `user_id` // query parameter consistent with the identity in the `Authorization` // header below. - options.queryParameters['user_id'] = _tokenManager.userId; + options.queryParameters['user_id'] = _effectiveTokenManager.userId; options.headers['Authorization'] = token.rawValue; options.headers['stream-auth-type'] = token.authType.headerValue; @@ -77,7 +95,7 @@ class AuthInterceptor extends QueuedInterceptor { final error = StreamApiError.fromJson(data); if (error.isTokenExpiredError) { - final tokenManager = _tokenManager; + final tokenManager = _effectiveTokenManager; // Don't try to refresh the token if we're using a static provider if (tokenManager.usesStaticProvider) return handler.next(err); // Otherwise, mark the current token as expired. diff --git a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart index 25340e83..37708067 100644 --- a/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart +++ b/packages/stream_core/test/api/interceptors/auth_interceptor_test.dart @@ -83,6 +83,37 @@ UserToken _generateTestUserToken(String userId) { void main() { group('AuthInterceptor', () { + test( + 'uses the TokenManager passed to the positional constructor, setting the ' + 'Authorization header and user_id query parameter (backwards-compatible ' + 'API)', + () async { + final tokenManager = TokenManager( + userId: 'user-123', + tokenProvider: TokenProvider.static( + _generateTestUserToken('user-123'), + ), + ); + + final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); + final adapter = _CapturingHttpClientAdapter(); + dio.httpClientAdapter = adapter; + dio.interceptors.add(AuthInterceptor(dio, tokenManager)); + + await dio.get('/test'); + + expect(adapter.lastRequest?.queryParameters['user_id'], 'user-123'); + expect( + adapter.lastRequest?.headers['Authorization'], + isNotNull, + ); + expect( + adapter.lastRequest?.headers['stream-auth-type'], + isNotNull, + ); + }, + ); + test( 'picks up a TokenManager swapped in while the token is loading, so the ' 'user_id query parameter reflects a server-resolved id (guest exchange)', @@ -107,7 +138,7 @@ void main() { final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); final adapter = _CapturingHttpClientAdapter(); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + dio.interceptors.add(AuthInterceptor.withProvider(dio, tokenManagerProvider: () => tokenManager)); await dio.get('/test'); @@ -132,7 +163,7 @@ void main() { final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); final adapter = _CapturingHttpClientAdapter(); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + dio.interceptors.add(AuthInterceptor.withProvider(dio, tokenManagerProvider: () => tokenManager)); await dio.get('/test'); @@ -153,7 +184,7 @@ void main() { final dio = Dio(BaseOptions(baseUrl: 'https://example.com')); final adapter = _TokenExpiredHttpClientAdapter(); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + dio.interceptors.add(AuthInterceptor.withProvider(dio, tokenManagerProvider: () => tokenManager)); await expectLater( dio.get('/test'), @@ -194,7 +225,7 @@ void main() { }, ); dio.httpClientAdapter = adapter; - dio.interceptors.add(AuthInterceptor(dio, () => tokenManager)); + dio.interceptors.add(AuthInterceptor.withProvider(dio, tokenManagerProvider: () => tokenManager)); await expectLater( dio.get('/test'), From 7943f1dbc84761f072748172833f809538dcdfe8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 14 Aug 2026 13:01:37 +0200 Subject: [PATCH 6/6] update changelog --- packages/stream_core/CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/stream_core/CHANGELOG.md b/packages/stream_core/CHANGELOG.md index 37f00237..bbd843d0 100644 --- a/packages/stream_core/CHANGELOG.md +++ b/packages/stream_core/CHANGELOG.md @@ -1,11 +1,8 @@ ## Upcoming -### 💥 BREAKING CHANGES - -- `AuthInterceptor` now takes a `TokenManager Function()` getter instead of a `TokenManager` instance. This lets callers swap the active `TokenManager` at runtime — e.g. after a guest token exchange resolves a server-assigned user id — and have the interceptor pick up the new instance (and its `userId`) on the next request. - ### ✨ Features +- Added `AuthInterceptor.withProvider`, which takes a `TokenManager Function()` getter instead of a fixed `TokenManager` instance. This lets callers swap the active `TokenManager` at runtime — e.g. after a guest token exchange resolves a server-assigned user id — and have the interceptor pick up the new instance (and its `userId`) on the next request. The existing `AuthInterceptor(dio, tokenManager)` constructor is unchanged. - Added `teams` field to `User` class. ## 0.4.0