refactor[epic-games]: migrate to GraphQL API and modularize authentication logic
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.
This commit is contained in:
parent
0d35a5ee85
commit
728b0c734b
6 changed files with 626 additions and 137 deletions
43
src/constants.ts
Normal file
43
src/constants.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Epic Games API Constants
|
||||
// Based on https://github.com/claabs/epicgames-freegames-node
|
||||
|
||||
export const EPIC_CLIENT_ID = '875a3b57d3a640a6b7f9b4e883463ab4';
|
||||
export const CSRF_ENDPOINT = 'https://www.epicgames.com/id/api/csrf';
|
||||
export const ACCOUNT_CSRF_ENDPOINT = 'https://www.epicgames.com/account/v2/refresh-csrf';
|
||||
export const ACCOUNT_SESSION_ENDPOINT = 'https://www.epicgames.com/account/personal';
|
||||
export const LOGIN_ENDPOINT = 'https://www.epicgames.com/id/api/login';
|
||||
export const REDIRECT_ENDPOINT = 'https://www.epicgames.com/id/api/redirect';
|
||||
export const GRAPHQL_ENDPOINT = 'https://store.epicgames.com/graphql';
|
||||
export const ARKOSE_BASE_URL = 'https://epic-games-api.arkoselabs.com';
|
||||
export const CHANGE_EMAIL_ENDPOINT = 'https://www.epicgames.com/account/v2/api/email/change';
|
||||
export const USER_INFO_ENDPOINT = 'https://www.epicgames.com/account/v2/personal/ajaxGet';
|
||||
export const RESEND_VERIFICATION_ENDPOINT = 'https://www.epicgames.com/account/v2/resendEmailVerification';
|
||||
export const REPUTATION_ENDPOINT = 'https://www.epicgames.com/id/api/reputation';
|
||||
export const STORE_CONTENT = 'https://store-content-ipv4.ak.epicgames.com/api/en-US/content';
|
||||
export const EMAIL_VERIFY = 'https://www.epicgames.com/id/api/email/verify';
|
||||
export const SETUP_MFA = 'https://www.epicgames.com/account/v2/security/ajaxUpdateTwoFactorAuthSettings';
|
||||
export const FREE_GAMES_PROMOTIONS_ENDPOINT = 'https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions';
|
||||
export const STORE_HOMEPAGE = 'https://store.epicgames.com/';
|
||||
export const STORE_HOMEPAGE_EN = `${STORE_HOMEPAGE}en-US/`;
|
||||
export const STORE_CART_EN = `${STORE_HOMEPAGE}en-US/cart`;
|
||||
export const ORDER_CONFIRM_ENDPOINT = 'https://payment-website-pci.ol.epicgames.com/purchase/confirm-order';
|
||||
export const ORDER_PREVIEW_ENDPOINT = 'https://payment-website-pci.ol.epicgames.com/purchase/order-preview';
|
||||
export const EPIC_PURCHASE_ENDPOINT = 'https://www.epicgames.com/store/purchase';
|
||||
export const MFA_LOGIN_ENDPOINT = 'https://www.epicgames.com/id/api/login/mfa';
|
||||
export const UNREAL_SET_SID_ENDPOINT = 'https://www.unrealengine.com/id/api/set-sid';
|
||||
export const TWINMOTION_SET_SID_ENDPOINT = 'https://www.twinmotion.com/id/api/set-sid';
|
||||
export const CLIENT_REDIRECT_ENDPOINT = `https://www.epicgames.com/id/api/client/${EPIC_CLIENT_ID}`;
|
||||
export const AUTHENTICATE_ENDPOINT = `https://www.epicgames.com/id/api/authenticate`;
|
||||
export const LOCATION_ENDPOINT = `https://www.epicgames.com/id/api/location`;
|
||||
export const PHASER_F_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/phaser/f';
|
||||
export const PHASER_BATCH_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/phaser/batch';
|
||||
export const TALON_IP_ENDPOINT = 'https://talon-service-v4-prod.ak.epicgames.com/v1/init/ip';
|
||||
export const TALON_INIT_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/init';
|
||||
export const TALON_EXECUTE_ENDPOINT = 'https://talon-service-v4-prod.ak.epicgames.com/v1/init/execute';
|
||||
export const TALON_WEBSITE_BASE = 'https://talon-website-prod.ak.epicgames.com';
|
||||
export const TALON_REFERRER = 'https://talon-website-prod.ak.epicgames.com/challenge?env=prod&flow=login_prod&origin=https%3A%2F%2Fwww.epicgames.com';
|
||||
export const ACCOUNT_OAUTH_TOKEN = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token';
|
||||
export const ACCOUNT_OAUTH_DEVICE_AUTH = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/deviceAuthorization';
|
||||
export const ID_LOGIN_ENDPOINT = 'https://www.epicgames.com/id/login';
|
||||
export const EULA_AGREEMENTS_ENDPOINT = 'https://eulatracking-public-service-prod-m.ol.epicgames.com/eulatracking/api/public/agreements';
|
||||
export const REQUIRED_EULAS = ['epicgames_privacy_policy_no_table', 'egstore'];
|
||||
171
src/cookie.ts
Normal file
171
src/cookie.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/device-auths.ts
Normal file
38
src/device-auths.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Device authentication management for Epic Games
|
||||
// Based on https://github.com/claabs/epicgames-freegames-node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { dataDir } from './util.js';
|
||||
|
||||
const CONFIG_DIR = dataDir('config');
|
||||
const deviceAuthsFilename = path.join(CONFIG_DIR, 'device-auths.json');
|
||||
|
||||
// Ensure config directory exists
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export async function getDeviceAuths() {
|
||||
try {
|
||||
const deviceAuths = JSON.parse(fs.readFileSync(deviceAuthsFilename, 'utf-8'));
|
||||
return deviceAuths;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAccountAuth(account) {
|
||||
const deviceAuths = await getDeviceAuths();
|
||||
return deviceAuths?.[account];
|
||||
}
|
||||
|
||||
export async function writeDeviceAuths(deviceAuths) {
|
||||
fs.writeFileSync(deviceAuthsFilename, JSON.stringify(deviceAuths, null, 2));
|
||||
}
|
||||
|
||||
export async function setAccountAuth(account, accountAuth) {
|
||||
const existingDeviceAuths = (await getDeviceAuths()) ?? {};
|
||||
existingDeviceAuths[account] = accountAuth;
|
||||
await writeDeviceAuths(existingDeviceAuths);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue