Files
plezy/lib/services/sensitive_prefs.dart
T
edde746 eaa1736c4e feat(mdblist): sync watched history, scrobbles and ratings with MDBList
Connects MDBList through its OAuth device-code grant, registered as a
Device Code app so no client secret or redirect URI ships in the binary
and TV, mobile and desktop all use the same flow.

MDBList omits `verification_uri_complete`, but its device page seeds the
code field from a `user_code` query parameter and the sign-in redirect
preserves the query string, so the activation link is built locally and
the dialog's open button lands on a filled-in form instead of an empty
one. A server-supplied complete URL still wins if one ever appears.

Poll state is read from the response body rather than the status code:
`authorization_pending` and `slow_down` both arrive as HTTP 400, and a
missing grant answers 404 `device_not_found`.

Writes go out as real-time `/scrobble/*` reports plus `/sync/watched`
for the marks that never pass through the player, with ratings on
`/sync/ratings`. Matching uses IMDb and TMDb only — MDBList's id block
has no `tvdb` field, so a TVDB-only item is skipped rather than written
under an empty id block.
2026-08-05 12:03:05 +02:00

78 lines
3.5 KiB
Dart

/// Preference keys whose stored values are credentials.
///
/// `shared_preferences` is the most credential-dense artifact in a Plezy
/// installation. On the desktop platforms it is a single plaintext JSON file
/// next to the database, and it holds:
///
/// * [credentialVaultKeyPref] — the AES-256 key that `CredentialVault` uses to
/// protect every server/profile token stored in the Drift
/// `connections.config_json` and `profile_connections.user_token` columns.
/// Losing it orphans every one of those ciphertexts permanently.
/// * tracker sessions — `TrackerAccountStore` persists `TrackerSession.encode()`
/// verbatim, so raw OAuth `access_token`/`refresh_token` pairs for MAL,
/// AniList, Simkl, Trakt and MDBList live here in plaintext.
/// * Seerr sessions — `SeerrSessionStore` persists a raw `connect.sid` cookie
/// alongside a vault-protected password.
/// * [legacyPlexTokenPref] — the pre-connection-registry Plex token slot. It is
/// drained by the connection migration but can linger on old installs.
///
/// Two subsystems consult this list, both added for #1732:
///
/// * the tolerant preference reads in `BaseSharedPreferencesService` must never
/// silently drop one of these keys — an unreadable credential has to surface
/// as an explicit repair prompt, not as a silent re-authentication;
/// * the corrupt-store repair in `PrefsRecovery` salvages exactly these keys
/// out of a damaged store before quarantining it.
///
/// Keep this list exhaustive. A credential slot that is missing here is
/// silently dropped on a type mismatch and silently lost on a repair.
///
/// This lives apart from `CredentialVault`, `TrackerAccountStore` and
/// `SeerrSessionStore` so `BaseSharedPreferencesService` can depend on it
/// without an import cycle.
library;
/// Key holding the base64 `CredentialVault` AES-256 key.
const String credentialVaultKeyPref = 'credential_vault_key_v1';
/// Legacy single-slot Plex token, superseded by the connection registry.
const String legacyPlexTokenPref = 'plex_token';
/// Unscoped base keys used by `TrackerAccountStore`, one per tracker service.
const List<String> trackerSessionBaseKeys = <String>[
'mal_session',
'anilist_session',
'simkl_session',
'trakt_session',
'mdblist_session',
];
/// Unscoped base key used by `SeerrSessionStore`.
const String seerrSessionBaseKey = 'seerr_session';
/// Every credential slot that is profile-scoped through `profileScopedPrefsKey`,
/// so a stored key is either the bare base key or `user_{scope}_{baseKey}`.
const List<String> profileScopedCredentialBaseKeys = <String>[...trackerSessionBaseKeys, seerrSessionBaseKey];
final RegExp _profileScopedCredentialPattern = RegExp(
'^(?:user_.+_)?(?:${profileScopedCredentialBaseKeys.join('|')})\$',
);
/// The unscoped base key [key] resolves to, or null when [key] is not a
/// profile-scoped credential slot.
String? profileScopedCredentialBaseKey(String key) {
if (!_profileScopedCredentialPattern.hasMatch(key)) return null;
for (final base in profileScopedCredentialBaseKeys) {
if (key == base || key.endsWith('_$base')) return base;
}
return null;
}
/// Whether [key] is a profile-scoped or global Seerr session slot.
bool isSeerrSessionPrefKey(String key) => profileScopedCredentialBaseKey(key) == seerrSessionBaseKey;
/// Whether [key] holds a credential and must never be dropped or exported
/// without an explicit, informed user decision.
bool isSensitivePrefKey(String key) =>
key == credentialVaultKeyPref || key == legacyPlexTokenPref || profileScopedCredentialBaseKey(key) != null;