This commit refactors epic-games.js to use the GraphQL API instead of the legacy promotions endpoint for retrieving free games. Key architectural improvements include: - Added modular authentication module (device-auths.ts) supporting persistent device auth tokens - Introduces cookie management module (cookie.ts) for persistent session handling - Extracts GraphQL query structures and API endpoints into constants.ts - Implements multiple fallback strategies: device auth login, token exchange, and fallback to standard login - Adds support for both GraphQL and promotions-based game discovery - Streamlines claim process with improved tracking and error handling - Removes legacy selectors and redundant logic Additionally, updates package.json to include TypeScript and reorganizes dependency order for better maintainability.
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
// Cookie management for Epic Games
|
|
// Based on https://github.com/claabs/epicgames-freegames-node
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import tough from 'tough-cookie';
|
|
import { filenamify } from './util.js';
|
|
import { dataDir } from './util.js';
|
|
|
|
const CONFIG_DIR = dataDir('config');
|
|
const DEFAULT_COOKIE_NAME = 'default';
|
|
|
|
// Ensure config directory exists
|
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
}
|
|
|
|
function getCookiePath(username) {
|
|
const fileSafeUsername = filenamify(username);
|
|
const cookieFilename = path.join(CONFIG_DIR, `${fileSafeUsername}-cookies.json`);
|
|
return cookieFilename;
|
|
}
|
|
|
|
// Cookie whitelist - only these cookies are stored
|
|
const COOKIE_WHITELIST = ['EPIC_SSO_RM', 'EPIC_SESSION_AP', 'EPIC_DEVICE'];
|
|
|
|
// Cookie jar cache
|
|
const cookieJars = new Map();
|
|
|
|
function getCookieJar(username) {
|
|
let cookieJar = cookieJars.get(username);
|
|
if (cookieJar) {
|
|
return cookieJar;
|
|
}
|
|
const cookieFilename = getCookiePath(username);
|
|
cookieJar = new tough.CookieJar();
|
|
cookieJars.set(username, cookieJar);
|
|
return cookieJar;
|
|
}
|
|
|
|
// Convert EditThisCookie format to tough-cookie file store format
|
|
export function editThisCookieToToughCookieFileStore(etc) {
|
|
const tcfs = {};
|
|
|
|
etc.forEach((etcCookie) => {
|
|
const domain = etcCookie.domain.replace(/^\./, '');
|
|
const expires = etcCookie.expirationDate
|
|
? new Date(etcCookie.expirationDate * 1000).toISOString()
|
|
: undefined;
|
|
const { path: cookiePath, name } = etcCookie;
|
|
|
|
if (COOKIE_WHITELIST.includes(name)) {
|
|
const temp = {
|
|
[domain]: {
|
|
[cookiePath]: {
|
|
[name]: {
|
|
key: name,
|
|
value: etcCookie.value,
|
|
expires,
|
|
domain,
|
|
path: cookiePath,
|
|
secure: etcCookie.secure,
|
|
httpOnly: etcCookie.httpOnly,
|
|
hostOnly: etcCookie.hostOnly,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
Object.assign(tcfs, temp);
|
|
}
|
|
});
|
|
|
|
return tcfs;
|
|
}
|
|
|
|
// Get cookies as simple object
|
|
export function getCookies(username) {
|
|
const cookieJar = getCookieJar(username);
|
|
const cookies = cookieJar.toJSON()?.cookies || [];
|
|
return cookies.reduce((accum, cookie) => {
|
|
if (cookie.key && cookie.value) {
|
|
return { ...accum, [cookie.key]: cookie.value };
|
|
}
|
|
return accum;
|
|
}, {});
|
|
}
|
|
|
|
// Get raw cookies in tough-cookie file store format
|
|
export async function getCookiesRaw(username) {
|
|
const cookieFilename = getCookiePath(username);
|
|
try {
|
|
const existingCookies = JSON.parse(fs.readFileSync(cookieFilename, 'utf8'));
|
|
return existingCookies;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Set cookies from Playwright/Cookie format
|
|
export async function setPuppeteerCookies(username, newCookies) {
|
|
const cookieJar = getCookieJar(username);
|
|
|
|
for (const cookie of newCookies) {
|
|
const domain = cookie.domain.replace(/^\./, '');
|
|
const tcfsCookie = new tough.Cookie({
|
|
key: cookie.name,
|
|
value: cookie.value,
|
|
expires: cookie.expires ? new Date(cookie.expires * 1000) : undefined,
|
|
domain,
|
|
path: cookie.path,
|
|
secure: cookie.secure,
|
|
httpOnly: cookie.httpOnly,
|
|
hostOnly: !cookie.domain.startsWith('.'),
|
|
});
|
|
|
|
try {
|
|
await cookieJar.setCookie(tcfsCookie, `https://${domain}`);
|
|
} catch (err) {
|
|
console.error('Error setting cookie:', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete cookies for a user
|
|
export async function deleteCookies(username) {
|
|
const cookieFilename = getCookiePath(username || DEFAULT_COOKIE_NAME);
|
|
try {
|
|
fs.unlinkSync(cookieFilename);
|
|
} catch {
|
|
// File doesn't exist, that's fine
|
|
}
|
|
}
|
|
|
|
// Check if user has a valid cookie
|
|
export async function userHasValidCookie(username, cookieName) {
|
|
const cookieFilename = getCookiePath(username);
|
|
try {
|
|
const fileExists = fs.existsSync(cookieFilename);
|
|
if (!fileExists) return false;
|
|
|
|
const cookieData = JSON.parse(fs.readFileSync(cookieFilename, 'utf8'));
|
|
const rememberCookieExpireDate = cookieData['epicgames.com']?.['/']?.[cookieName]?.expires;
|
|
if (!rememberCookieExpireDate) return false;
|
|
|
|
return new Date(rememberCookieExpireDate) > new Date();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Convert imported cookies (EditThisCookie format)
|
|
export async function convertImportCookies(username) {
|
|
const cookieFilename = getCookiePath(username);
|
|
const fileExists = fs.existsSync(cookieFilename);
|
|
|
|
if (fileExists) {
|
|
try {
|
|
const cookieData = fs.readFileSync(cookieFilename, 'utf8');
|
|
const cookieTest = JSON.parse(cookieData);
|
|
|
|
if (Array.isArray(cookieTest)) {
|
|
// Convert from EditThisCookie format
|
|
const tcfsCookies = editThisCookieToToughCookieFileStore(cookieTest);
|
|
fs.writeFileSync(cookieFilename, JSON.stringify(tcfsCookies, null, 2));
|
|
}
|
|
} catch {
|
|
// Invalid format, delete file
|
|
fs.unlinkSync(cookieFilename);
|
|
}
|
|
}
|
|
}
|