Let the desktop provision its sync account from the wrapper
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
release-apk / release (APK) (push) Waiting to run

Same change todo just took, for the same reason: app/'s desktop target is a
Flutter web build in a Chrome --app window, so FlutterSecureStorage resolves
to the web backend (AES-encrypted localStorage, keyed inside the browser
profile). Nothing outside the page can write that store — `secret-tool`
targets libsecret, which only a Linux embedder build would read, and a Dart
CLI cannot even import flutter_secure_storage, since it is a Flutter plugin
that fails to resolve on the VM. Both verified rather than assumed.

The wrapper is the one process that can read the filesystem and serve the
page, which is exactly why it already holds the GitHub token instead of the
browser (see github_proxy.dart). So the sync account now arrives the same
way: loadAccount() falls back to GET /sync-account when the store is empty
and persists the result, making the route a one-shot provisioning step
rather than a standing dependency.

It reads ~/.config/crdt-sync/{firebase.json,password} — the same pair
`diet_guard sync` already uses — so the machine keeps one source of truth.

Off unless CRDT_SYNC_SERVE_ACCOUNT is set, and 404 (not 403) when disabled so
a probe cannot tell the route exists. On Android there is no wrapper: the
request simply fails and loadAccount() returns null exactly as before.

clearAccount() writes an opt-out marker the fallback checks, so Disconnect
cannot be silently undone by the next launch re-adopting the account.

518 tests, analyzer clean, pre-commit clean. Pinned to crdt_sync_dart-v0.8.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
This commit is contained in:
Krzysztof Rudnicki 2026-08-09 21:41:08 +02:00
parent 3ff08f93f6
commit 55fade1e15
4 changed files with 70 additions and 5 deletions

View File

@ -18,8 +18,11 @@
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:diet_guard_app/desktop/github_proxy.dart';
import 'package:diet_guard_app/services/desktop_wrapper.dart';
import 'package:path/path.dart' as p;
@ -93,9 +96,60 @@ class WrapperServer {
path.substring(WrapperPaths.documents.length),
);
}
if (path == kSyncAccountPath) {
return _syncAccount(request);
}
return _static(request, path);
}
/// Serves the shared sync account so a desktop install can self-provision.
///
/// Off unless CRDT_SYNC_SERVE_ACCOUNT is set: this hands out a credential
/// with database write access to anything that can reach the port, so it is
/// something you switch on once to set an install up, not a standing route.
/// 404 (rather than 403) when disabled, so a probe cannot tell it exists.
///
/// Reads the same ~/.config/crdt-sync/ pair `diet_guard sync` already uses,
/// so the machine has one source of truth rather than a per-app copy —
/// the same reasoning that keeps the GitHub token in [gitHubProxy] instead
/// of the browser.
Future<void> _syncAccount(HttpRequest request) async {
if ((Platform.environment[kSyncAccountEnvVar] ?? '').isEmpty) {
request.response.statusCode = HttpStatus.notFound;
return;
}
final configDir = p.join(
Platform.environment['HOME'] ?? '',
'.config',
'crdt-sync',
);
final configFile = File(p.join(configDir, 'firebase.json'));
final passwordFile = File(p.join(configDir, 'password'));
if (!configFile.existsSync() || !passwordFile.existsSync()) {
stderr.writeln(
'Sync account requested but ~/.config/crdt-sync/ is incomplete — '
'the desktop app will keep asking for credentials.',
);
request.response.statusCode = HttpStatus.notFound;
return;
}
final email =
(jsonDecode(await configFile.readAsString())
as Map<String, dynamic>)['email'];
if (email is! String || email.isEmpty) {
stderr.writeln('firebase.json has no usable "email" — not serving it.');
request.response.statusCode = HttpStatus.notFound;
return;
}
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode({
'email': email,
'password': (await passwordFile.readAsString()).trim(),
}),
);
}
/// GET returns the stored bytes (404 when absent); POST overwrites them.
///
/// [name] is rejected unless it is a plain filename: these handlers write

View File

@ -47,9 +47,18 @@ SecureCredentialStore credentialStore() => SecureCredentialStore(
/// Reads the per-device account, or null when sync has not been set up.
Future<FirebaseAccount?> loadAccount() async {
try {
return FirebaseAccount.tryParse(
final stored = FirebaseAccount.tryParse(
await _secure.read(key: kFirebaseAccountKey),
);
if (stored != null) return stored;
// Disconnect must stick: without this the next launch would silently
// re-adopt the account and the button would look broken.
if (await _secure.read(key: kSyncAccountOptOutKey) != null) return null;
// Desktop only in practice: on Android the wrapper does not exist, the
// request fails, and this returns null exactly as before.
final provisioned = await accountFromWrapper(Uri.base);
if (provisioned != null) await saveAccount(provisioned);
return provisioned;
} on Object {
// No secret service available: behave as "not configured" rather than
// crashing the settings screen.
@ -64,6 +73,8 @@ Future<void> saveAccount(FirebaseAccount account) =>
/// Forgets the account and any cached session.
Future<void> clearAccount() async {
await _secure.delete(key: kFirebaseAccountKey);
// Suppress wrapper re-provisioning; see loadAccount().
await _secure.write(key: kSyncAccountOptOutKey, value: 'true');
await credentialStore().clear();
}

View File

@ -85,11 +85,11 @@ packages:
dependency: "direct main"
description:
path: crdt_sync_dart
ref: "crdt_sync_dart-v0.7.0"
resolved-ref: "6eef99d66132e12eb5056ae8e1780b34237fa013"
ref: "crdt_sync_dart-v0.8.0"
resolved-ref: "6a080333c2ed2b2773fee01605dd67ba713e54ea"
url: "https://github.com/kuhyx/utils"
source: git
version: "0.7.0"
version: "0.8.0"
crypto:
dependency: "direct main"
description:

View File

@ -11,7 +11,7 @@ dependencies:
crdt_sync:
git:
url: https://github.com/kuhyx/utils
ref: crdt_sync_dart-v0.7.0
ref: crdt_sync_dart-v0.8.0
path: crdt_sync_dart
crypto: ^3.0.6
flutter: