* feat(playback): forward portal Cookie/Authorization to built-in players
The web players (HTML5/hls.js, Video.js, ArtPlayer, Shaka) could only ever
receive User-Agent/Referer/Origin, so any Stalker stream gated on the portal
session cookie or Bearer token played exclusively in external MPV/VLC — the
long-running "only VLC works" cluster (#849, #910, #732).
- request-header-overrides.service: the scoped override now carries Cookie
and Authorization, attached only to requests on the exact stream origin,
in-memory only, dropped on replace/clear. Unscoped (playlist-level) calls
drop credentials fail-closed; control characters in header values are
rejected. Chosen over session.cookies.set(): jar cookies attach only to
credentialed requests, which would force withCredentials into every engine
and break against the Access-Control-Allow-Origin:* IPTV panels send, and
jar scoping is port-blind.
- WebPlayerViewComponent is now the single owner of the scoped override for
every built-in player: it extracts the full header set from the resolved
playback, configures the override BEFORE handing the source over (players
render only once the source exists), and clears the scoped layer on
destroy. HtmlVideoPlayerComponent's own three-header call is removed — it
would overwrite the credentialed override.
- Stalker VOD, series episodes and radio now build the same portal header
set ITV already had (they previously carried no portal headers at all);
same-origin playback sends the real User-Agent alongside X-User-Agent.
- Stream classification is host-based via one shared predicate
(isStalkerStreamCredentialSafe): same-host port changes and scheme
upgrades keep the portal profile (the #1158 class), a foreign host or
https->http downgrade keeps the credential-free KSPlayer profile. The
main-process fallback context uses the same predicate so
isStalkerDirectStreamProfile can no longer discard renderer headers.
- setUserAgent bridge gains an optional credentials parameter; preload,
ipcMain handler and ElectronBridgeApi updated together.
- stalker-mock-server: gated-stream scenario (MAC 00:1A:79:00:00:09) whose
create_link returns a local /stream/gated/video.mp4 that 403s without the
mac cookie + current Bearer token; new Electron e2e proves a built-in
player actually plays it (and that the gate refuses bare requests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): apply header override to Stalker radio, redact mock cookie log
Address Codex review feedback on #1335:
- The radio branch of the Stalker live layout renders the dedicated audio
player, never WebPlayerViewComponent, so the resolved portal headers were
built but never applied — an auth-gated radio stream still 403'd. The
override sync is extracted into ElectronStreamHeadersService (single owner
of the scoped override slot, with clear-only-while-owning semantics so a
destroyed consumer cannot wipe a newer consumer's override), applied by
WebPlayerViewComponent for video players and by the radio branch before
the audio element gets its URL. The service feature-detects the bridge
method so partial bridges behave like the PWA instead of throwing.
- The gated-stream mock no longer logs the raw Cookie header on 403 —
presence only, matching the Authorization logging.
- The gated scenario now serves an audio fixture for radio create_link and
the Electron e2e covers the radio path end-to-end (bare request 403s,
built-in audio player advances past the gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): claim radio header ownership before awaiting the IPC
Codex round-2 P2: leaving the radio route while the header IPC was still in
flight left the portal cookie/token installed — ngOnDestroy saw a null scope
URL (it was recorded only after the await) and could not clear the override.
Ownership is now claimed synchronously before awaiting, destroy invalidates
the pending playback continuation, and the apply's stillCurrent verdict is
honored. Regression test covers destroy-during-pending-IPC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): carry portal headers into collection playback
Codex round-3 P1: Stalker channels opened from Favorites/Recently Viewed
resolved through StreamResolverService.resolveStalker(), which returned no
portal headers — the video path handed the header owner an empty set and
collection radio bypassed it entirely, so auth-gated streams still 403'd
from collections.
- resolveStalker() now builds the same profile as the live layout via the
shared classifier: portal-owned streams get mac cookie/Bearer token/MAG
UA/portal Origin+Referer, foreign hosts keep the credential-free KSPlayer
profile (both create_link results and direct radio URLs).
- UnifiedLiveTabComponent applies the scoped override for radio before the
audio element gets its URL (ownership claimed before awaiting the IPC,
round-2 lesson), and clears it on close and destroy.
- Regression tests: resolver header profiles for portal-host and foreign
streams; unified tab radio apply-then-clear.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): release the radio override when a new selection mounts no player
Codex round-4 P2: after radio installed its credentials, selecting an item
that never mounts a player surface (external video playback, failed
resolution) left the old Cookie/Authorization installed — no
WebPlayerViewComponent, close, or destroy cleanup runs on that path. Both
radio hosts (unified collection tab and the Stalker live layout, which has
the identical hole) now release the previously owned radio scope at the
start of every new selection; the slot-ownership semantics keep this a
no-op when another playback already owns the override. Regression test in
the live-layout spec pins the failed-selection path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(playback): state the exact override release points
Codex round-5 P2 flagged that the media 'ended' event does not clear the
scoped override while the player stays mounted. That is deliberate, not a
gap: a mounted player still owns the session — replay or a seek into an
unbuffered range must keep working against a gated stream, and clearing on
'ended' would 403 exactly the streams this PR fixes. The credentials only
ever travel to the exact origin that issued them, and every dismount path
(channel/source change, player close/destroy, radio close, playerless
selection) releases them. The security doc and the release note now say
precisely that instead of the ambiguous "cleared when playback ends".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(playback): fit the release note back under the 400-character cap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): keep session headers on same-host redirects
Since 0.22 requestWithValidatedRedirects stripped Cookie/Authorization
whenever a redirect changed the *origin*, so a portal answering with an
http->https upgrade or a port move lost the MAC cookie and Bearer token
mid-session. Real Stalker/Ministra servers then reply with a plain-text
"Authorization failed." body: categories fail to load, create_link never
resolves, and no player receives a stream URL (#1158 regression window).
Scope credential stripping to the host instead: same-host scheme/port
redirects keep headers, basic auth, params, and request bodies; a
redirect to a different host still drops all of them, preserving the
original hardening intent (no credential leaks to third-party hosts).
Also adds the Stalker API compatibility roadmap produced by the
2026-08-01 protocol audit (.plans/, force-added like earlier plans).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(electron): strip credentials on same-host https-to-http downgrades
Review follow-up (Greptile P1 + Codex on #1322): the host-only check
kept Authorization/Cookie/basic auth/params/body when an https request
was redirected to http on the same host, replaying a TLS-obtained
session in cleartext. Treat that downgrade like a host change: strip
credentials and refuse to replay request bodies. Scheme upgrades and
port moves on the same host keep headers — the actual #1158 scenarios.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(security): harden Electron IPC against MITM, SSRF, path and injection risks
S1 TLS: validate certs by default on playlist/EPG fetches (opt-out via IPTVNATOR_ALLOW_INSECURE_TLS); new util/secure-https.ts.
S2: write-file IPC restricted to save-dialog-authorized paths.
S3: XTREAM_PROBE_URL guarded by assertRemoteUrlAllowed + maxRedirects:0; new events/url-safety.ts (+19 tests).
S4: EPG titles rendered via interpolation, not [innerHTML].
S5: downloads reveal/play limited to recorded download paths.
S6: Stalker cmd encoded (slash-preserving) to block query injection.
EPG-worker and Stalker fetches reject file://-style/credentialed URLs; LAN/self-hosted targets remain allowed.
* perf(player): lazy-load web video players via @defer
Wrap Video.js/HTML5/ArtPlayer in @defer (on immediate) so video.js, hls.js,
artplayer and mpegts.js split into a deferred chunk loaded on first playback
instead of eagerly on the player route. Embedded MPV (native) stays eager.
Spec uses DeferBlockBehavior.Playthrough.
* fix(player): remove leaked HTML video listeners on destroy
volumechange used a mismatched removeEventListener reference, while
loadedmetadata and timeupdate were never removed at all. Bind all three to
stable handler fields used for both add and remove, and add a teardown
regression test asserting each listener is detached on destroy.
* refactor(dashboard): extract pure navigation helpers from DashboardDataService
Move the 8 stateless link/navigation-state/type-kind helpers into a new
dashboard-navigation.util.ts so the routing logic is independently testable and
the 1260-line god-service shrinks. DashboardDataService keeps the public methods
as thin delegators (facade) so the public API and the single consumer
(workspace-dashboard-rails) are unchanged. First slice of the DashboardDataService
decomposition; verified by the existing service spec (33/33) and the app typecheck.
* fix(review): address PR feedback (IPv6 link-local, write-path cap, @defer placeholder)
- url-safety: broaden IPv6 link-local detection to the full fe80::/10 range
(fe80:: through febf::), not just the fe80:: prefix (+ regression tests).
- playlist.events: cap authorizedWritePaths (evict oldest past 32) so a save
dialog opened without a following write cannot accumulate entries until restart.
- web-player-view: add a @placeholder to each @defer (on immediate) player block
to avoid the one-frame blank/layout-shift before the chunk resolves.
* fix(security): close Electron network and download gaps
* test(downloads): cover cancellation and restart cleanup
* fix(downloads): address Greptile review gaps
* test(security): reproduce remaining Greptile findings
* fix(security): close remaining Greptile findings
* test(downloads): reproduce early database queue stall
* fix(downloads): release queue after setup failures
* test(downloads): reproduce completion queue stall
* fix(downloads): release queue after completion failures