fix(stalker): key every session by endpoint, identity and credentials

Four review findings, all the same root cause — the session key was not applied
consistently:

- The in-run token cache still used an identity-only key, so editing the portal
  URL without restarting returned the cached token and sent that bearer to the
  newly configured host. Both caches now use one key.
- Credentials were in neither key, so changing a status-2 portal's login kept
  serving the previous account's session indefinitely.
- A stored token with NO recorded fingerprint was accepted. Rows written before
  the fingerprint existed carry exactly that, and re-presenting one after an
  edit is the disclosure the fingerprint prevents. Missing now counts as
  unverified; such a row owes a full profile anyway, so nothing is lost.
- `StreamResolverService` read `playlist.isFullStalkerPortal` directly instead
  of the shared predicate, so a legacy row with an absent flag but a canonical
  URL skipped authentication — a restored older backup opened a direct-URL
  radio favorite with no Bearer header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-03 18:41:55 +02:00
1 parent d2f28334de
commit 226ffbb9e6
7 files changed
+142 -46

No files matched your search

+11 -5
View File
@@ -268,11 +268,17 @@ A renegotiated session is written back best-effort
The handshake's `not_valid` flag is propagated into the follow-up
`get_profile` as `not_valid_token`.
Reuse is gated on identity. The fingerprint the session was negotiated for is
persisted next to the token (`Playlist.stalkerSessionIdentity`), and a token
whose fingerprint no longer matches the playlist is never re-presented — an
edited MAC, serial or device id must not inherit the previous session, which
is the same rule the in-memory cache enforces for the current run.
Reuse is gated on a session fingerprint (`stalkerSessionFingerprint`) covering
the **portal origin, the device identity and the account credentials**, stored
next to the token as `Playlist.stalkerSessionIdentity` and used for the
in-run cache as well, so an edit applies without a restart. All three halves
are load-bearing: `ensureToken()` re-presents tokens in a handshake, so an
endpoint edit would otherwise disclose the previous portal's bearer token to
another host; an identity edit must not inherit the old session; and for a
status-2 portal the login decides which account the token represents. A token
with no recorded fingerprint (written before this existed) counts as
unverified and is never re-presented — such a row owes a full profile anyway,
and the write-back then records the fingerprint.
Because that reuse skips the only response carrying the watchdog cadence, the
cadence is persisted **with** the token (`Playlist.stalkerWatchdogTimeout` /