fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store

- A write that removes a playlist's last lock clears the SQLite index
  first and drops the store key afterwards, so an interruption between the
  two can only leave a state the startup reconcile repairs toward locked.
- A PIN hash read failure is distinct from an absent PIN: the session stays
  locked and every PIN-protected step re-reads it first.
- Backup export awaits parental lock initialization and refuses to run
  while the lock store is not readable, since an absent lock field means
  "no opinion" on restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-26 06:57:27 +02:00
1 parent 02f5b09201
commit 4f931030e1
9 files changed
+195 -23

No files matched your search

+11 -2
View File
@@ -45,7 +45,12 @@ The PIN is hashed with PBKDF2-SHA256 through WebCrypto
(`parental-lock-pin.util.ts`, format `v1$<iterations>$<salt>$<hash>`), so
Electron and PWA share one implementation. The hash is deliberately not part
of `Settings`: settings are logged, backed up and mirrored to the main
process.
process. A hash that could not be READ is not an absent PIN:
`ParentalLockStorageService.readPinHash()` reports the failure as `null`
(an absent PIN is `{hash: null}`), the session then stays locked (`enabled`
treats an unreadable PIN as set while the switch is unknown) and every
PIN-protected step — unlock, change PIN, disable — re-reads it first, so
storage answering later is enough to recover without a restart.
The lock store (`ParentalLockStore` in `parental-lock.util.ts`) is keyed by
playlist id and holds, per playlist, Xtream `{categoryType, xtreamId}`
@@ -269,7 +274,11 @@ on either side.
as locked while Electron reads, which filter by the index alone, still
serve it); if the rollback write or its re-stamp fails too, the playlist is
re-stamped on the next store access, and every launch re-derives the
index from the store for each playlist that has locks — awaited inside
index from the store for each playlist that has locks — which is why a
write that removes a playlist's LAST lock clears the index first and
drops the key afterwards (the reconcile finds playlists only through
their key; an interruption then leaves store-with-lock and index-without,
which the next reconcile repairs toward locked) — awaited inside
the store's `load()`, so `readable` (and with it every catalog read the
renderer gates) stays false until the index agrees with the store; a
re-stamp that keeps failing keeps the session fail-closed.