Agent API accepts X-Tray-Key pushes; Electron tray captures cookies and upserts accounts by email.
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import { session as electronSession } from "electron";
|
|
import type { TrayAccount, TrayConfig } from "./config";
|
|
|
|
export type CapturedTokens = {
|
|
session: string;
|
|
accessToken: string;
|
|
profileId: string | null;
|
|
persistentLogin: string | null;
|
|
accessTokenExpMs: number | null;
|
|
};
|
|
|
|
const COOKIE_DOMAINS = [
|
|
"viaplay.com",
|
|
".viaplay.com",
|
|
"login.viaplay.com",
|
|
"account.mtg-api.com",
|
|
".mtg-api.com",
|
|
];
|
|
|
|
function jwtExpMs(token: string): number | null {
|
|
try {
|
|
const payload = JSON.parse(
|
|
Buffer.from(token.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
|
|
) as { exp?: number };
|
|
return payload.exp ? payload.exp * 1000 : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function captureViaplayTokens(partition: string): Promise<CapturedTokens | null> {
|
|
const ses = electronSession.fromPartition(`persist:${partition}`);
|
|
const byName = new Map<string, string>();
|
|
for (const domain of COOKIE_DOMAINS) {
|
|
try {
|
|
const cookies = await ses.cookies.get({ domain });
|
|
for (const c of cookies) {
|
|
if (c.name && c.value) byName.set(c.name, c.value);
|
|
}
|
|
} catch {
|
|
/* ignore domain miss */
|
|
}
|
|
}
|
|
|
|
// Also scan without domain filter
|
|
try {
|
|
const all = await ses.cookies.get({});
|
|
for (const c of all) {
|
|
if (!c.name || !c.value) continue;
|
|
if (
|
|
/viaplay|mtg-api|login\.viaplay/i.test(c.domain || "") ||
|
|
["session", "accessToken", "viaplay_profileId", "persistentLogin"].includes(c.name)
|
|
) {
|
|
byName.set(c.name, c.value);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
const sessionVal = byName.get("session")?.trim() || "";
|
|
const accessToken = (byName.get("accessToken") || "")
|
|
.trim()
|
|
.replace(/^(VIAPLAY-AT|MTG-AT|Bearer)\s+/i, "");
|
|
const profileId = byName.get("viaplay_profileId")?.trim() || null;
|
|
const persistentLogin = byName.get("persistentLogin")?.trim() || null;
|
|
|
|
if (!sessionVal || !accessToken) return null;
|
|
return {
|
|
session: sessionVal,
|
|
accessToken,
|
|
profileId,
|
|
persistentLogin,
|
|
accessTokenExpMs: jwtExpMs(accessToken),
|
|
};
|
|
}
|
|
|
|
export async function pushViaplayTokens(
|
|
cfg: TrayConfig,
|
|
account: TrayAccount,
|
|
tokens: CapturedTokens
|
|
): Promise<{ summary: string; subscription?: string }> {
|
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Tray-Key": cfg.trayKey,
|
|
},
|
|
body: JSON.stringify({
|
|
email: account.email,
|
|
label: account.label || undefined,
|
|
session: tokens.session,
|
|
accessToken: tokens.accessToken,
|
|
profileId: tokens.profileId || undefined,
|
|
persistentLogin: tokens.persistentLogin || undefined,
|
|
}),
|
|
});
|
|
const data = (await res.json().catch(() => ({}))) as {
|
|
error?: { message?: string };
|
|
accounts?: Array<{ email?: string; subscriptionSummary?: string | null }>;
|
|
};
|
|
if (!res.ok) {
|
|
throw new Error(data.error?.message || `HTTP ${res.status}`);
|
|
}
|
|
const match = data.accounts?.find((a) => a.email === account.email);
|
|
const sub = match?.subscriptionSummary || undefined;
|
|
return {
|
|
summary: sub ? `ok · ${sub}` : "ok · tokens geüpdatet",
|
|
subscription: sub,
|
|
};
|
|
}
|
|
|
|
export async function pingAgent(cfg: TrayConfig): Promise<boolean> {
|
|
const res = await fetch(`${cfg.apiUrl}/api/v1/agent/status`, {
|
|
headers: { "X-Tray-Key": cfg.trayKey },
|
|
});
|
|
return res.ok;
|
|
}
|