todo-app/lib/cli/file_credential_store.dart
Krzysztof kuhy Rudnicki 56cb1b3594
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Add headless note CLI (bin/todo_note.dart) reusing app CRDT sync
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 15:35:46 +02:00

53 lines
1.9 KiB
Dart

/// The headless CLI's Firebase session, kept in a file of its own.
library;
import 'dart:convert';
import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
/// A file-backed [FirebaseCredentialStore], seeded once from [seedFile].
///
/// [seedFile] is todo's desktop session (`~/.config/todo/firebase_auth.json`,
/// written by `crdt-sync/tool/seed_session.py --app todo`) and is only ever
/// read. Every write goes to [file], a private copy in the CLI's data dir.
/// That split matters: [FirebaseTokenProvider] clears its store when a refresh
/// token is revoked, and a shared file would let one bad CLI run delete the
/// seed the desktop wrapper provisions itself from.
class FileCredentialStore implements FirebaseCredentialStore {
/// Creates a store that persists to [file], falling back to [seedFile].
FileCredentialStore({required this.file, required this.seedFile});
/// The CLI's own copy, written on every token refresh.
final File file;
/// The read-only seed used until [file] exists.
final File seedFile;
@override
Future<FirebaseCredentials?> load() async {
final source = file.existsSync() ? file : seedFile;
if (!source.existsSync()) return null;
final decoded = jsonDecode(await source.readAsString());
if (decoded is! Map<String, dynamic>) return null;
return FirebaseCredentials.fromJson(decoded);
}
@override
Future<void> save(FirebaseCredentials credentials) async {
await file.parent.create(recursive: true);
final tmp = File('${file.path}.$pid.tmp');
// Owner-only before the token is written, not after: dart:io has no
// chmod, and a umask-default file would briefly expose a DB credential.
await tmp.writeAsString('');
await Process.run('chmod', ['600', tmp.path]);
await tmp.writeAsString(jsonEncode(credentials.toJson()));
await tmp.rename(file.path);
}
@override
Future<void> clear() async {
if (file.existsSync()) await file.delete();
}
}