diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index a22f5045..ca3c6e4f 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -1075,15 +1075,8 @@ extension AppControllerDesktopRuntimeHelpers on AppController { normalizedHost == bridgeHost && (bridgePort <= 0 || endpoint.port == bridgePort); if (matchesBridgeEndpoint) { - final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN'); - if (envToken != null && envToken.isNotEmpty) { - return envToken; - } - - final bridgeToken = (await storeInternal.loadAccountManagedSecret( - target: kAccountManagedSecretTargetBridgeAuthToken, - ))?.trim(); - if (bridgeToken?.isNotEmpty == true) { + final bridgeToken = await _resolveManagedBridgeAuthTokenInternal(); + if (bridgeToken != null && bridgeToken.isNotEmpty) { return bridgeToken; } } @@ -1100,20 +1093,27 @@ extension AppControllerDesktopRuntimeHelpers on AppController { return null; } - final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN'); - if (envToken != null && envToken.isNotEmpty) { - return _normalizeAuthorizationHeaderInternal(envToken); - } - - final bridgeToken = (await storeInternal.loadAccountManagedSecret( - target: kAccountManagedSecretTargetBridgeAuthToken, - ))?.trim(); - if (bridgeToken?.isNotEmpty == true) { - return _normalizeAuthorizationHeaderInternal(bridgeToken!); + final bridgeToken = await _resolveManagedBridgeAuthTokenInternal(); + if (bridgeToken != null && bridgeToken.isNotEmpty) { + return _normalizeAuthorizationHeaderInternal(bridgeToken); } return null; } + Future _resolveManagedBridgeAuthTokenInternal() async { + final accountSyncState = settingsControllerInternal.accountSyncState; + if (settingsControllerInternal.accountSignedIn && + accountSyncState?.tokenConfigured.bridge == true) { + final bridgeToken = (await storeInternal.loadAccountManagedSecret( + target: kAccountManagedSecretTargetBridgeAuthToken, + ))?.trim(); + return bridgeToken?.isNotEmpty == true ? bridgeToken : null; + } + + final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN'); + return envToken?.isNotEmpty == true ? envToken : null; + } + int? gatewayProfileIndexMatchingEndpointInternal(Uri endpoint) { final normalizedHost = endpoint.host.trim().toLowerCase(); final normalizedScheme = endpoint.scheme.trim().toLowerCase(); diff --git a/test/runtime/bridge_runtime_cleanup_test.dart b/test/runtime/bridge_runtime_cleanup_test.dart index b38c7f45..2cc09115 100644 --- a/test/runtime/bridge_runtime_cleanup_test.dart +++ b/test/runtime/bridge_runtime_cleanup_test.dart @@ -116,6 +116,16 @@ void main() { enableSecureStorage: false, ); await store.initialize(); + await store.saveAccountSessionToken('session-token'); + await store.saveAccountSessionSummary( + const AccountSessionSummary( + userId: 'user-1', + email: 'review@svc.plus', + name: 'Review User', + role: 'reviewer', + mfaEnabled: true, + ), + ); await store.saveAccountManagedSecret( target: kAccountManagedSecretTargetBridgeAuthToken, value: 'bridge-token', diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 1edc4b03..ffed2ab6 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -1247,6 +1247,72 @@ void main() { }, ); + test( + 'desktop bridge auth resolver prefers synced managed bridge token over stale environment token', + () async { + final storeRoot = await Directory.systemTemp.createTemp( + 'xworkmate-acp-auth-managed-bridge-env-priority-', + ); + addTearDown(() async { + if (await storeRoot.exists()) { + try { + await storeRoot.delete(recursive: true); + } on FileSystemException { + // Temp cleanup is best effort here. The controller may still be + // releasing files when teardown starts. + } + } + }); + + final store = SecureConfigStore( + secretRootPathResolver: () async => '${storeRoot.path}/secrets', + appDataRootPathResolver: () async => '${storeRoot.path}/app-data', + supportRootPathResolver: () async => '${storeRoot.path}/support', + enableSecureStorage: false, + ); + await store.initialize(); + await store.saveAccountSessionToken('session-token'); + await store.saveAccountSessionSummary( + const AccountSessionSummary( + userId: 'user-1', + email: 'review@svc.plus', + name: 'Review User', + role: 'reviewer', + mfaEnabled: true, + ), + ); + await store.saveAccountSyncState( + AccountSyncState.defaults().copyWith( + syncState: 'ready', + tokenConfigured: const AccountTokenConfigured( + bridge: true, + vault: false, + ), + ), + ); + await store.saveAccountManagedSecret( + target: kAccountManagedSecretTargetBridgeAuthToken, + value: 'fresh-bridge-token', + ); + + final controller = AppController( + environmentOverride: const { + 'BRIDGE_AUTH_TOKEN': 'stale-env-token', + }, + store: store, + ); + addTearDown(controller.dispose); + await controller.settingsControllerInternal.initialize(); + + final header = await controller + .resolveGatewayAcpAuthorizationHeaderInternal( + Uri.parse('https://xworkmate-bridge.svc.plus/acp/rpc'), + ); + + expect(header, 'fresh-bridge-token'); + }, + ); + test( 'desktop bridge auth resolver does not fallback to the remote gateway token for bridge ACP', () async { diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart index 9912eff6..5951127f 100644 --- a/test/runtime/runtime_controllers_settings_account_test.dart +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -388,6 +388,16 @@ void main() { enableSecureStorage: false, ); await store.initialize(); + await store.saveAccountSessionToken('session-token'); + await store.saveAccountSessionSummary( + const AccountSessionSummary( + userId: 'user-1', + email: 'review@svc.plus', + name: 'Review User', + role: 'reviewer', + mfaEnabled: true, + ), + ); await store.saveAccountSyncState( AccountSyncState.defaults().copyWith( syncState: 'ready',