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