From 277e0a0385d3e347fbb20ff448a4b964e09778ae Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 3 Aug 2026 10:11:15 +0200 Subject: [PATCH] fix(stalker): warm the session against the repaired portal configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while auditing my own static branch against the create_link path rather than waiting for the next review round. `executeStalkerRequest` applies the lazy-repair override on its first line, so the create_link path always talks to the configuration a completed repair proved good. The session warm-up I added did not: it handed `ensureToken` the caller's pre-repair row, so a portal whose endpoint or mode had been repaired would handshake against the configuration the repair had already rejected — stranding the session precisely on the portals repair exists to rescue. The override now happens inside `ensureStalkerSession`, mirroring `executeStalkerRequest`'s first line, so every caller inherits the rule instead of each having to remember it. Mutation-checked: dropping the override fails the new test alone. Co-Authored-By: Claude Opus 5 --- .../lib/collection/stream-resolver.service.ts | 4 +- .../stalker-player-request.utils.spec.ts | 38 +++++++++++++++++++ .../lib/stores/utils/stalker-request.utils.ts | 17 ++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts index 9a3301011..21dec92d3 100644 --- a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts +++ b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts @@ -375,7 +375,9 @@ export class StreamResolverService { // `ensureToken` performs the handshake + get_profile — and // validates the identity the cached token was negotiated for, // which the raw `getCachedToken` below cannot — without minting a - // link; a simple portal returns null immediately. + // link; a simple portal returns null immediately. The raw row goes + // in: the helper applies the repair override itself, exactly as + // `executeStalkerRequest` does on the branch below. await this.warmStalkerSession(playlist); return this.buildStalkerPlayback(item, playlist, { diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts index 10ac7b3b5..e000df3cc 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts @@ -214,6 +214,44 @@ describe('stalker-player-request.utils', () => { expect(dataService.sendIpcEvent).not.toHaveBeenCalled(); }); + it('handshakes against a repaired endpoint, not the stale one', async () => { + // `executeStalkerRequest` applies the repair override on its first + // line, so the static path must too — handshaking against the + // configuration a repair has already proven broken would strand + // the session. + const stale = { + ...PLAYLIST, + portalUrl: 'http://demo.example/portal.php', + isFullStalkerPortal: true, + } as PlaylistMeta; + const repaired = { + ...stale, + portalUrl: 'http://demo.example/stalker_portal/server/load.php', + } as PlaylistMeta; + + await fetchStalkerPlaybackLink( + { + dataService: dataService as never, + stalkerSession: stalkerSession as StalkerSessionService, + portalRepair: { + applyOverride: jest.fn().mockReturnValue(repaired), + shouldAttemptRepair: jest.fn().mockReturnValue(false), + repairPortal: jest.fn().mockResolvedValue(null), + }, + }, + { + playlist: stale, + selectedContentType: 'itv', + cmd: 'ffrt3 http://cdn.example/live/42.m3u8', + linkFlags: { use_http_tmp_link: '0' }, + } + ); + + expect(stalkerSession.ensureToken).toHaveBeenCalledWith( + expect.objectContaining({ portalUrl: repaired.portalUrl }) + ); + }); + it('skips the handshake for a simple portal', async () => { await fetchStalkerPlaybackLink(deps(), { playlist: PLAYLIST, diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts index 50624eca8..84abdc06c 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts @@ -59,13 +59,26 @@ export async function ensureStalkerSession( playlist: PlaylistMeta | undefined, logger?: { warn(...args: unknown[]): void } ): Promise { - if (!playlist || !isFullStalkerPortalPlaylist(playlist)) { + if (!playlist) { + return; + } + + // The repair override is applied here for the same reason + // `executeStalkerRequest` applies it on its first line: a completed repair + // may have moved the endpoint or the mode while the caller still holds the + // pre-repair row, and handshaking against the configuration a repair has + // already proven broken would strand the session. + const effective = deps.portalRepair + ? deps.portalRepair.applyOverride(playlist) + : playlist; + + if (!isFullStalkerPortalPlaylist(effective)) { return; } try { await deps.stalkerSession.ensureToken( - toStalkerSessionPlaylist(playlist) + toStalkerSessionPlaylist(effective) ); } catch (error) { logger?.warn('Could not establish the Stalker session', error);