The wrong-type recovery only covered reads that went through a BaseSharedPreferencesService instance. The three stores that hold credentials read the shared cache directly, so a mistyped value there still threw a raw TypeError or, for Seerr, was swallowed by a catch-all and reported as "no session" — the registry documented protection it did not actually provide. readPreferenceTolerantly now takes the cache, so CredentialVault, TrackerAccountStore and SeerrSessionStore get the same classification as the settings layer. CredentialVault's post-write re-read moves outside its catch: a wrong-typed value written by another isolate was swallowed there, and the process then returned a key that never durably landed, making every ciphertext written under it unreadable on the next launch. Those stores are consulted long after startup, where a throw is an unhandled provider error rather than a repair prompt, so SettingsService initialization now walks the cached key set once and reads every sensitive key. That puts the failure inside a fatal gate step while the store is still open and a surgical single-key repair is possible. The remaining direct reads in settings and storage are routed too; the only ones left are the library-density dual-type migration, which probes both types deliberately, and an untyped switch that is type-safe by construction.
47 lines
1.8 KiB
Dart
47 lines
1.8 KiB
Dart
import '../../models/seerr/seerr_session.dart';
|
|
import '../../profiles/profile.dart';
|
|
import '../base_shared_preferences_service.dart';
|
|
import '../credential_vault.dart';
|
|
|
|
/// Per-Plex-profile persistence for the Seerr session, mirroring
|
|
/// `TrackerAccountStore`'s `user_{uuid}_{baseKey}` scoping.
|
|
///
|
|
/// The password ([SeerrSession.secret]) is CredentialVault-protected at the
|
|
/// store boundary; a failed decrypt degrades to an empty secret (the session
|
|
/// keeps working until its cookie expires) rather than dropping the session.
|
|
class SeerrSessionStore {
|
|
static const String _baseKey = 'seerr_session';
|
|
|
|
const SeerrSessionStore();
|
|
|
|
String _scopedKey(String userUuid) => profileScopedPrefsKey(userUuid, _baseKey);
|
|
|
|
Future<SeerrSession?> load(String userUuid) async {
|
|
final prefs = await BaseSharedPreferencesService.sharedCache();
|
|
// Outside the try below on purpose: an unreadable credential must
|
|
// reach the repair prompt, not be swallowed as 'no session'.
|
|
final raw = readTolerantString(prefs, _scopedKey(userUuid));
|
|
if (raw == null) return null;
|
|
try {
|
|
final session = SeerrSession.decode(raw);
|
|
if (session.secret.isEmpty) return session;
|
|
return session.copyWith(secret: await CredentialVault.reveal(session.secret) ?? '');
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> save(String userUuid, SeerrSession session) async {
|
|
final prefs = await BaseSharedPreferencesService.sharedCache();
|
|
final protected = session.secret.isEmpty
|
|
? session
|
|
: session.copyWith(secret: await CredentialVault.protect(session.secret));
|
|
await prefs.setString(_scopedKey(userUuid), protected.encode());
|
|
}
|
|
|
|
Future<void> clear(String userUuid) async {
|
|
final prefs = await BaseSharedPreferencesService.sharedCache();
|
|
await prefs.remove(_scopedKey(userUuid));
|
|
}
|
|
}
|