From f19c173369e327286fb3288d588ef5bd9af73a8e Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 22 Sep 2026 22:47:40 +0200 Subject: [PATCH] Fix Viaplay overview Forbidden by using Bearer-only auth. Match manage-account HAR: no cookies on overview, and surface expired accessToken clearly. --- apps/master-api/src/viaplay/client.ts | 131 +++++++++++++++----------- 1 file changed, 74 insertions(+), 57 deletions(-) diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 3c16d32..8d2de4d 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -73,79 +73,87 @@ export type ViaplayOverview = { sessionOk?: boolean; }; +function normalizeAccessToken(raw: string | null | undefined): string | null { + if (!raw) return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + return trimmed.replace(/^Bearer\s+/i, "").trim() || null; +} + +function accessTokenExpiryInfo(token: string): { + expired: boolean; + expMs: number | null; +} { + try { + const payload = JSON.parse( + Buffer.from(token.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString() + ) as { exp?: number }; + if (!payload.exp) return { expired: false, expMs: null }; + const expMs = payload.exp * 1000; + return { expired: Date.now() >= expMs, expMs }; + } catch { + return { expired: false, expMs: null }; + } +} + +/** + * Pakketinfo komt van account.mtg-api.com/overview. + * Browser stuurt alleen Authorization: Bearer (geen cookies) — cookies geven Forbidden. + */ export async function fetchAccountOverview( creds: ViaplayAccountCreds, timeoutMs: number ): Promise { - const baseHeaders: Record = { - Accept: "application/json, text/plain, */*", + const token = normalizeAccessToken(creds.accessToken); + if (!token) { + return { + ok: false, + status: 0, + partnerName: null, + productNames: [], + summary: "", + error: "Geen accessToken", + sessionOk: false, + }; + } + + const tokenInfo = accessTokenExpiryInfo(token); + // Exact zoals manage-account in de browser (HAR): Bearer-only + Content-Type + const headers: Record = { + Accept: "*/*", "Content-Type": "application/json", - "Accept-Language": "nl-NL,nl;q=0.9,en;q=0.8", + "Accept-Language": "nl,en-US;q=0.7,en;q=0.3", Origin: "https://viaplay.com", Referer: "https://viaplay.com/", "User-Agent": VIAPLAY_USER_AGENT, - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "cross-site", + Authorization: `Bearer ${token}`, }; - const cookie = [ - creds.session ? `session=${creds.session}` : "", - creds.accessToken ? `accessToken=${creds.accessToken}` : "", - creds.profileId ? `viaplay_profileId=${creds.profileId}` : "", - ] - .filter(Boolean) - .join("; "); - - const attempts: Array<{ label: string; url: string; headers: Record }> = [ - { - label: "cookie+bearer", - url: "https://account.mtg-api.com/overview", - headers: { - ...baseHeaders, - Cookie: cookie, - ...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}), - }, - }, - { - label: "cookie", - url: "https://account.mtg-api.com/overview", - headers: { ...baseHeaders, Cookie: cookie }, - }, - { - label: "bearer+deviceKey", - url: "https://account.mtg-api.com/overview?deviceKey=pcdash-nl", - headers: { - ...baseHeaders, - ...(creds.accessToken ? { Authorization: `Bearer ${creds.accessToken}` } : {}), - ...(creds.profileId ? { Cookie: `viaplay_profileId=${creds.profileId}` } : {}), - }, - }, - ]; - let lastStatus = 0; let lastError = "Overview mislukt"; - for (const attempt of attempts) { - try { - const res = await fetchJson(attempt.url, { - headers: attempt.headers, - timeoutMs: Math.min(timeoutMs, 15_000), - }); - lastStatus = res.status; - const body = asRecord(res.json); - if (res.status === 200 && body) { - const parsed = parseOverviewBody(body); - if (parsed.ok && parsed.summary) return parsed; - lastError = "Overview gaf geen pakketinformatie terug"; - continue; - } + try { + const res = await fetchJson("https://account.mtg-api.com/overview", { + headers, + timeoutMs: Math.min(timeoutMs, 15_000), + }); + lastStatus = res.status; + const body = asRecord(res.json); + if (res.status === 200 && body) { + const parsed = parseOverviewBody(body); + if (parsed.ok && parsed.summary) return parsed; + lastError = "Overview gaf geen pakketinformatie terug"; + } else { lastError = (typeof body?.message === "string" && body.message) || (typeof body?.userMessage === "string" && body.userMessage) || - `Overview HTTP ${res.status}`; - } catch (err) { - lastError = err instanceof Error ? err.message : String(err); + (res.status === 403 + ? "Forbidden" + : res.status === 401 + ? "Unauthorized" + : `Overview HTTP ${res.status}`); } + } catch (err) { + lastError = err instanceof Error ? err.message : String(err); } let sessionOk = false; @@ -164,6 +172,15 @@ export async function fetchAccountOverview( /* ignore */ } + if ( + sessionOk && + (lastStatus === 401 || lastStatus === 403 || /forbidden|unauthorized/i.test(lastError)) + ) { + lastError = tokenInfo.expired + ? "accessToken verlopen — plak een verse accessToken uit de browser en Check opnieuw" + : "overview Forbidden — plak een verse accessToken (Bearer) uit de browser; sessiecookie is niet genoeg"; + } + return { ok: false, status: lastStatus,