diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 9e101e6b..38d0638e 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -1016,6 +1016,16 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } Uri? resolveBridgeAcpEndpointInternal() { + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + final selfHostedUrl = selfHosted.serverUrl.trim(); + if (selfHosted.isConfigured && selfHostedUrl.isNotEmpty) { + final uri = Uri.tryParse(selfHostedUrl); + if (uri != null && uri.hasScheme && uri.host.trim().isNotEmpty) { + return uri.replace(query: null, fragment: null); + } + } + final uri = Uri.parse(kManagedBridgeServerUrl); return uri.replace(query: null, fragment: null); } @@ -1029,6 +1039,11 @@ extension AppControllerDesktopRuntimeHelpers on AppController { if (bridgeEndpoint == null) { return false; } + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + if (selfHosted.isConfigured) { + return true; + } final accountSyncState = settingsControllerInternal.accountSyncState; if (settingsControllerInternal.accountSignedIn && accountSyncState?.tokenConfigured.bridge == true) { @@ -1072,6 +1087,10 @@ extension AppControllerDesktopRuntimeHelpers on AppController { normalizedHost == bridgeHost && (bridgePort <= 0 || endpoint.port == bridgePort); if (matchesBridgeEndpoint) { + final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal(); + if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) { + return manualBridgeToken; + } final bridgeToken = await _resolveManagedBridgeAuthTokenInternal(); if (bridgeToken != null && bridgeToken.isNotEmpty) { return bridgeToken; @@ -1097,6 +1116,22 @@ extension AppControllerDesktopRuntimeHelpers on AppController { return null; } + Future _resolveManualBridgeAuthTokenInternal() async { + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + if (!selfHosted.isConfigured) { + return null; + } + final passwordRef = selfHosted.passwordRef.trim(); + if (passwordRef.isEmpty) { + return null; + } + final token = (await storeInternal.loadSecretValueByRef( + passwordRef, + ))?.trim(); + return token?.isNotEmpty == true ? token : null; + } + Future _resolveManagedBridgeAuthTokenInternal() async { final accountSyncState = settingsControllerInternal.accountSyncState; if (settingsControllerInternal.accountSignedIn && diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart index 5951127f..e5167247 100644 --- a/test/runtime/runtime_controllers_settings_account_test.dart +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -441,6 +441,71 @@ void main() { }, ); + test('manual bridge config becomes the runtime ACP source', () async { + final storeRoot = await Directory.systemTemp.createTemp( + 'xworkmate-manual-bridge-runtime-', + ); + 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.saveSettingsSnapshot( + SettingsSnapshot.defaults().copyWith( + acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults() + .copyWith( + selfHosted: AcpBridgeServerModeConfig.defaults().selfHosted + .copyWith( + serverUrl: 'https://private-bridge.svc.plus', + username: 'admin', + ), + ), + ), + ); + await store.saveSecretValueByRef( + AcpBridgeServerSelfHostedConfig.defaults().passwordRef, + 'manual-bridge-token', + ); + + final controller = AppController( + environmentOverride: const {}, + store: store, + ); + addTearDown(controller.dispose); + await controller.settingsControllerInternal.initialize(); + + expect( + controller.resolveGatewayAcpEndpointInternal()?.toString(), + 'https://private-bridge.svc.plus', + ); + expect(controller.isBridgeAcpRuntimeConfiguredInternal(), isTrue); + expect( + await controller.resolveGatewayAcpAuthorizationHeaderInternal( + Uri.parse('https://private-bridge.svc.plus/acp/rpc'), + ), + 'manual-bridge-token', + ); + expect( + await controller.resolveGatewayAcpAuthorizationHeaderInternal( + Uri.parse('$kManagedBridgeServerUrl/acp/rpc'), + ), + isNull, + ); + }); + test( 'syncAccountSettings succeeds when bridge url metadata is missing', () async {