feat: Add OAuth Device Flow login to bypass Cloudflare
All checks were successful
build-and-push / lint (push) Successful in 9s
build-and-push / sonar (push) Successful in 21s
build-and-push / docker (push) Successful in 13s

- src/device-login.js: New module implementing Epic Games OAuth Device Flow
- src/logger.js: Simple logger module for consistent logging
- src/config.js: Add deviceAuthClientId and deviceAuthSecret config
- epic-claimer-new.js: Use OAuth Device Flow instead of browser login
- Cloudflare bypass: Device Flow uses API, user logs in own browser
- Based on: https://github.com/claabs/epicgames-freegames-node

How it works:
1. Get client credentials from Epic OAuth API
2. Get device authorization code with verification URL
3. Send user notification with login link
4. User clicks link and logs in (handles Cloudflare manually)
5. Poll for authorization completion
6. Save and use access/refresh tokens
7. Tokens auto-refresh on expiry

Benefits:
- No Cloudflare issues (no bot detection)
- Persistent tokens (no repeated logins)
- Works in headless mode
- More reliable than browser automation
This commit is contained in:
root 2026-03-08 14:26:44 +00:00
parent f1d647bcb2
commit 393f70d409
4 changed files with 361 additions and 103 deletions

64
src/logger.js Normal file
View file

@ -0,0 +1,64 @@
/**
* Simple logger for free-games-claimer
*/
const LOG_LEVELS = {
trace: 0,
debug: 1,
info: 2,
warn: 3,
error: 4,
};
const currentLevel = process.env.LOG_LEVEL
? LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()]
: LOG_LEVELS.info;
function formatMessage(level, module, message, data) {
const timestamp = new Date().toISOString();
const moduleStr = module ? `[${module}] ` : '';
const dataStr = data && Object.keys(data).length > 0 ? ' ' + JSON.stringify(data) : '';
return `${timestamp} ${level.toUpperCase().padEnd(5)} ${moduleStr}${message}${dataStr}`;
}
function createLogger(module) {
return {
trace: (dataOrMessage, message) => {
if (currentLevel <= LOG_LEVELS.trace) {
const [data, msg] = typeof dataOrMessage === 'string' ? [null, dataOrMessage] : [dataOrMessage, message];
console.log(formatMessage('trace', module, msg || '', data));
}
},
debug: (dataOrMessage, message) => {
if (currentLevel <= LOG_LEVELS.debug) {
const [data, msg] = typeof dataOrMessage === 'string' ? [null, dataOrMessage] : [dataOrMessage, message];
console.log(formatMessage('debug', module, msg || '', data));
}
},
info: (dataOrMessage, message) => {
if (currentLevel <= LOG_LEVELS.info) {
const [data, msg] = typeof dataOrMessage === 'string' ? [null, dataOrMessage] : [dataOrMessage, message];
console.log(formatMessage('info', module, msg || '', data));
}
},
warn: (dataOrMessage, message) => {
if (currentLevel <= LOG_LEVELS.warn) {
const [data, msg] = typeof dataOrMessage === 'string' ? [null, dataOrMessage] : [dataOrMessage, message];
console.log(formatMessage('warn', module, msg || '', data));
}
},
error: (dataOrMessage, message) => {
if (currentLevel <= LOG_LEVELS.error) {
const [data, msg] = typeof dataOrMessage === 'string' ? [null, dataOrMessage] : [dataOrMessage, message];
console.log(formatMessage('error', module, msg || '', data));
}
},
child: childData => {
const childModule = childData?.module || module;
return createLogger(childModule);
},
};
}
const logger = createLogger('root');
export default logger;