fix: prioritize managed bridge sync state
This commit is contained in:
parent
57f1cbc02a
commit
7e4b2a756a
@ -101,14 +101,14 @@ flowchart TD
|
||||
D --> C
|
||||
C --> E["bridge runtime"]
|
||||
|
||||
note1["Priority order\n1. selfHosted when explicitly configured\n2. cloudSynced when account sync is ready and token exists\n3. disconnected"] --> C
|
||||
note1["Priority order\n1. cloudSynced when account sync is ready and token exists\n2. selfHosted when explicitly configured\n3. disconnected or managed default"] --> C
|
||||
```
|
||||
|
||||
### Runtime Invariants
|
||||
|
||||
- `selfHosted` always wins when it is configured.
|
||||
- `cloudSynced` is valid only when account sync is ready and the managed bridge token exists.
|
||||
- Signed-out state is disconnected: runtime must not use a default managed endpoint, stale managed secret, gateway profile token, or loopback ACP endpoint.
|
||||
- `cloudSynced` wins when account sync is ready and the managed bridge token exists.
|
||||
- `selfHosted` wins only when cloud sync is not ready.
|
||||
- Signed-out state is disconnected for authorization decisions: runtime must not use a stale managed secret, gateway profile token, or loopback ACP endpoint.
|
||||
- Missing `BRIDGE_AUTH_TOKEN` is disconnected for the managed cloud-sync path.
|
||||
- `BRIDGE_SERVER_URL` may be retained in `AccountSyncState.syncedDefaults.bridgeServerUrl`, but it is metadata only.
|
||||
- `BRIDGE_AUTH_TOKEN` is written to secure storage only, never to normal settings.
|
||||
|
||||
@ -1248,6 +1248,17 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
|
||||
Uri? resolveBridgeAcpEndpointInternal() {
|
||||
final accountSyncState = settingsControllerInternal.accountSyncState;
|
||||
final managedBridgeReady =
|
||||
settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
|
||||
accountSyncState?.tokenConfigured.bridge == true;
|
||||
if (managedBridgeReady) {
|
||||
return Uri.parse(kManagedBridgeServerUrl);
|
||||
}
|
||||
|
||||
final selfHosted = settingsControllerInternal
|
||||
.snapshot
|
||||
.acpBridgeServerModeConfig
|
||||
@ -1260,8 +1271,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
}
|
||||
|
||||
final uri = Uri.parse(kManagedBridgeServerUrl);
|
||||
return uri.replace(query: null, fragment: null);
|
||||
return Uri.parse(kManagedBridgeServerUrl);
|
||||
}
|
||||
|
||||
Uri? resolveExternalAcpEndpointForTargetInternal(AssistantExecutionTarget _) {
|
||||
@ -1281,12 +1291,16 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
return true;
|
||||
}
|
||||
final accountSyncState = settingsControllerInternal.accountSyncState;
|
||||
if (settingsControllerInternal.accountSignedIn &&
|
||||
if (settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
|
||||
accountSyncState?.tokenConfigured.bridge == true) {
|
||||
return true;
|
||||
}
|
||||
if (settingsControllerInternal.accountSignedIn) {
|
||||
if (settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN');
|
||||
@ -1322,19 +1336,28 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
|
||||
final bridgeHost = bridgeEndpoint?.host.trim().toLowerCase() ?? '';
|
||||
final bridgePort = bridgeEndpoint?.port ?? 0;
|
||||
final accountSyncState = settingsControllerInternal.accountSyncState;
|
||||
final managedBridgeReady =
|
||||
settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
|
||||
accountSyncState?.tokenConfigured.bridge == true;
|
||||
final matchesBridgeEndpoint =
|
||||
bridgeHost.isNotEmpty &&
|
||||
normalizedHost == bridgeHost &&
|
||||
(bridgePort <= 0 || endpoint.port == bridgePort);
|
||||
if (matchesBridgeEndpoint) {
|
||||
if (managedBridgeReady) {
|
||||
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
|
||||
if (bridgeToken != null && bridgeToken.isNotEmpty) {
|
||||
return bridgeToken;
|
||||
}
|
||||
}
|
||||
final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal();
|
||||
if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) {
|
||||
return manualBridgeToken;
|
||||
}
|
||||
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
|
||||
if (bridgeToken != null && bridgeToken.isNotEmpty) {
|
||||
return bridgeToken;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -1345,18 +1368,28 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
final normalizedHost = endpoint.host.trim().toLowerCase();
|
||||
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
|
||||
final bridgeHost = bridgeEndpoint?.host.trim().toLowerCase() ?? '';
|
||||
final accountSyncState = settingsControllerInternal.accountSyncState;
|
||||
final managedBridgeReady =
|
||||
settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
|
||||
accountSyncState?.tokenConfigured.bridge == true;
|
||||
if (bridgeHost.isEmpty || normalizedHost != bridgeHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (managedBridgeReady) {
|
||||
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
|
||||
if (bridgeToken != null && bridgeToken.isNotEmpty) {
|
||||
return _normalizeAuthorizationHeaderInternal(bridgeToken);
|
||||
}
|
||||
}
|
||||
|
||||
final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal();
|
||||
if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) {
|
||||
return _normalizeAuthorizationHeaderInternal(manualBridgeToken);
|
||||
}
|
||||
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
|
||||
if (bridgeToken != null && bridgeToken.isNotEmpty) {
|
||||
return _normalizeAuthorizationHeaderInternal(bridgeToken);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1379,15 +1412,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
|
||||
Future<String?> _resolveManagedBridgeAuthTokenInternal() async {
|
||||
if (settingsControllerInternal.accountSignedIn) {
|
||||
if (settingsControllerInternal.accountSessionTokenInternal
|
||||
.trim()
|
||||
.isNotEmpty) {
|
||||
final bridgeToken = (await storeInternal.loadAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
))?.trim();
|
||||
return bridgeToken?.isNotEmpty == true ? bridgeToken : null;
|
||||
}
|
||||
if (settingsControllerInternal.accountSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN');
|
||||
return envToken?.isNotEmpty == true ? envToken : null;
|
||||
|
||||
@ -49,8 +49,8 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
text: '2000',
|
||||
);
|
||||
|
||||
bool _useGpu = false;
|
||||
bool _adaptiveResolution = false;
|
||||
final bool _useGpu = false;
|
||||
final bool _adaptiveResolution = false;
|
||||
bool _showControlPanel = true;
|
||||
String _connectionState = 'disconnected';
|
||||
bool _hasStream = false;
|
||||
|
||||
@ -603,6 +603,20 @@ AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
|
||||
SettingsController controller, {
|
||||
required AcpBridgeServerModeConfig config,
|
||||
}) {
|
||||
final accountSyncState = controller.accountSyncState;
|
||||
final managedBridgeReady =
|
||||
controller.accountSessionTokenInternal.trim().isNotEmpty &&
|
||||
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
|
||||
accountSyncState?.tokenConfigured.bridge == true;
|
||||
if (managedBridgeReady) {
|
||||
return const AcpBridgeServerEffectiveConfig(
|
||||
endpoint: kManagedBridgeServerUrl,
|
||||
tokenRef: '',
|
||||
source: 'cloud',
|
||||
reason: 'Account sync is ready and the managed bridge token is available',
|
||||
);
|
||||
}
|
||||
|
||||
if (config.selfHosted.isConfigured) {
|
||||
return AcpBridgeServerEffectiveConfig(
|
||||
endpoint: config.selfHosted.serverUrl,
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
void main() {
|
||||
group('Bridge runtime cleanup', () {
|
||||
test(
|
||||
'keeps the managed bridge endpoint fixed even when account sync carries a bridge URL',
|
||||
'keeps the managed bridge endpoint fixed even when account sync carries a bridge URL and stale manual bridge config exists',
|
||||
() async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-bridge-runtime-cleanup-',
|
||||
@ -48,6 +48,28 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
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.saveSettingsSnapshot(
|
||||
SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||
.copyWith(
|
||||
selfHosted: AcpBridgeServerModeConfig.defaults().selfHosted
|
||||
.copyWith(
|
||||
serverUrl: 'https://acp-bridge.onwalk.net',
|
||||
username: 'admin',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await store.saveAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
value: 'bridge-token',
|
||||
@ -66,6 +88,12 @@ void main() {
|
||||
controller.resolveBridgeAcpEndpointInternal()?.toString(),
|
||||
kManagedBridgeServerUrl,
|
||||
);
|
||||
expect(
|
||||
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
|
||||
Uri.parse('$kManagedBridgeServerUrl/acp/rpc'),
|
||||
),
|
||||
'bridge-token',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.resolveExternalAcpEndpointForTargetInternal(
|
||||
|
||||
@ -490,6 +490,14 @@ void main() {
|
||||
),
|
||||
'fresh-bridge-token',
|
||||
);
|
||||
expect(
|
||||
controller.snapshot.acpBridgeServerModeConfig.effective.source,
|
||||
'cloud',
|
||||
);
|
||||
expect(
|
||||
controller.snapshot.acpBridgeServerModeConfig.effective.endpoint,
|
||||
kManagedBridgeServerUrl,
|
||||
);
|
||||
expect(
|
||||
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
|
||||
'https://xworkmate-bridge-new.svc.plus',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user