diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index 6f4aed8..51b113f 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -9,7 +9,6 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte import { EarnablePoints } from '../interface/Points' import { QuizData } from '../interface/QuizData' import { saveSessionData } from '../util/Load' -import { logError } from '../util/Logger' export default class BrowserFunc { @@ -114,7 +113,7 @@ export default class BrowserFunc { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'GO-HOME', `[goHome] Navigation failed: ${errorMessage}`, 'error') throw error } } @@ -179,7 +178,7 @@ export default class BrowserFunc { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Error fetching dashboard data: ${errorMessage}`, 'error') + this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `[getDashboardData] Failed to fetch dashboard data: ${errorMessage}`, 'error') throw error } @@ -246,7 +245,7 @@ export default class BrowserFunc { /** * Parse dashboard object from script content - * FIXED: Added format validation before JSON.parse + * IMPROVED: Enhanced validation with structure checks */ private async parseDashboardFromScript(page: Page, scriptContent: string): Promise { return await page.evaluate((scriptContent: string) => { @@ -262,16 +261,27 @@ export default class BrowserFunc { try { const jsonStr = match[1] // Validate basic JSON structure before parsing - if (!jsonStr.trim().startsWith('{') || !jsonStr.trim().endsWith('}')) { + const trimmed = jsonStr.trim() + if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) { continue } + const parsed = JSON.parse(jsonStr) - // Validate it's actually an object + + // Enhanced validation: check structure and type if (typeof parsed !== 'object' || parsed === null) { continue } + + // Validate essential dashboard properties exist + if (!parsed.userStatus || typeof parsed.userStatus !== 'object') { + continue + } + + // Successfully validated dashboard structure return parsed } catch (e) { + // JSON.parse failed or validation error - try next pattern continue } } @@ -339,7 +349,7 @@ export default class BrowserFunc { } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', `[getBrowserEarnablePoints] Failed to calculate earnable points: ${errorMessage}`, 'error') throw error } } @@ -404,7 +414,7 @@ export default class BrowserFunc { return points } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', `[getAppEarnablePoints] Failed to fetch app earnable points: ${errorMessage}`, 'error') throw error } } @@ -420,7 +430,7 @@ export default class BrowserFunc { return data.userStatus.availablePoints } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', `[getCurrentPoints] Failed to fetch current points: ${errorMessage}`, 'error') throw error } } @@ -493,7 +503,7 @@ export default class BrowserFunc { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `[getQuizData] Failed to extract quiz data: ${errorMessage}`, 'error') throw error } @@ -561,7 +571,7 @@ export default class BrowserFunc { this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!') } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred: ' + errorMessage, 'error') + this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `[closeBrowser] Failed to close browser cleanly: ${errorMessage}`, 'error') throw error } } diff --git a/src/constants.ts b/src/constants.ts index b1e1979..9a04607 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,7 +5,7 @@ /** * Parse environment variable as number with validation - * FIXED: Added strict validation for min/max boundaries + * FIXED: Added strict validation for min/max boundaries with logging * @param key Environment variable name * @param defaultValue Default value if parsing fails or out of range * @param min Minimum allowed value @@ -18,13 +18,24 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num const parsed = Number(raw) // Strict validation: must be finite, not NaN, and within bounds - if (!Number.isFinite(parsed) || parsed < min || parsed > max) { + if (!Number.isFinite(parsed)) { + console.warn(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`) + return defaultValue + } + + if (parsed < min || parsed > max) { + console.warn(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`) return defaultValue } return parsed } +// Login timeout boundaries (in milliseconds) +const LOGIN_TIMEOUT_MIN_MS = 30000 // 30 seconds - minimum login wait +const LOGIN_TIMEOUT_MAX_MS = 600000 // 10 minutes - maximum login wait +const LOGIN_TIMEOUT_DEFAULT_MS = 180000 // 3 minutes - default login timeout + export const TIMEOUTS = { SHORT: 500, MEDIUM: 1500, @@ -33,7 +44,7 @@ export const TIMEOUTS = { VERY_LONG: 5000, EXTRA_LONG: 10000, DASHBOARD_WAIT: 10000, - LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', 180000, 30000, 600000), + LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', LOGIN_TIMEOUT_DEFAULT_MS, LOGIN_TIMEOUT_MIN_MS, LOGIN_TIMEOUT_MAX_MS), NETWORK_IDLE: 5000, ONE_MINUTE: 60000, ONE_HOUR: 3600000, diff --git a/src/flows/DesktopFlow.ts b/src/flows/DesktopFlow.ts index 2eee53e..02d3547 100644 --- a/src/flows/DesktopFlow.ts +++ b/src/flows/DesktopFlow.ts @@ -29,8 +29,23 @@ export class DesktopFlow { /** * Execute the full desktop automation flow for an account - * @param account Account to process - * @returns Points collected during the flow + * + * Performs the following tasks in sequence: + * 1. Browser initialization with fingerprinting + * 2. Microsoft account login with 2FA support + * 3. Daily set completion + * 4. More promotions (quizzes, polls, etc.) + * 5. Punch cards + * 6. Desktop searches + * + * @param account Account to process (email, password, totp, proxy) + * @returns Promise resolving to points collected during the flow + * @throws {Error} If critical operation fails (login, browser init) + * + * @example + * const flow = new DesktopFlow(bot) + * const result = await flow.run(account) + * console.log(`Collected ${result.collectedPoints} points`) */ async run(account: Account): Promise { this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow') @@ -63,7 +78,10 @@ export class DesktopFlow { undefined, 0xFFAA00 ) - } catch {/* ignore */} + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + this.bot.log(false, 'DESKTOP-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn') + } // Save session for convenience, but do not close the browser try { diff --git a/src/flows/MobileFlow.ts b/src/flows/MobileFlow.ts index 9442cd1..8f85548 100644 --- a/src/flows/MobileFlow.ts +++ b/src/flows/MobileFlow.ts @@ -31,9 +31,24 @@ export class MobileFlow { /** * Execute the full mobile automation flow for an account - * @param account Account to process - * @param retryTracker Retry tracker for mobile search failures - * @returns Points collected during the flow + * + * Performs the following tasks in sequence: + * 1. Mobile browser initialization with mobile user agent + * 2. Microsoft account login + * 3. OAuth token acquisition for mobile API access + * 4. Daily check-in via mobile API + * 5. Read to earn articles + * 6. Mobile searches with retry logic + * + * @param account Account to process (email, password, totp, proxy) + * @param retryTracker Retry tracker for mobile search failures (auto-created if not provided) + * @returns Promise resolving to points collected during mobile flow + * @throws {Error} If critical operation fails (login, OAuth) + * + * @example + * const flow = new MobileFlow(bot) + * const result = await flow.run(account) + * console.log(`Mobile: ${result.collectedPoints} points`) */ async run( account: Account, @@ -69,7 +84,10 @@ export class MobileFlow { undefined, 0xFFAA00 ) - } catch {/* ignore */} + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + this.bot.log(true, 'MOBILE-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn') + } try { await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, true) diff --git a/src/functions/Activities.ts b/src/functions/Activities.ts index f97dc83..eb0d03c 100644 --- a/src/functions/Activities.ts +++ b/src/functions/Activities.ts @@ -39,6 +39,24 @@ export class Activities { } // Centralized dispatcher for activities from dashboard/punchcards + /** + * Execute a promotional activity (quiz, poll, search-on-bing, etc.) + * + * Automatically detects activity type and delegates to specialized handler: + * - quiz → Quiz handler + * - abc → ABC (drag-and-drop) handler + * - thisorthat → This or That handler + * - poll → Poll handler + * - urlreward → URL reward handler + * + * @param page Playwright page for activity execution + * @param activity Activity metadata from dashboard data + * @returns Promise resolving when activity is complete + * @throws {Error} If activity type is unsupported or execution fails + * + * @example + * await activities.run(page, dailySetActivity) + */ async run(page: Page, activity: MorePromotion | PromotionalItem): Promise { // First, try custom handlers (if any) for (const h of this.handlers) { diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts index 9be4a58..17fc2a2 100644 --- a/src/functions/activities/Search.ts +++ b/src/functions/activities/Search.ts @@ -1,11 +1,11 @@ -import { Page } from 'rebrowser-playwright' import { platform } from 'os' +import { Page } from 'rebrowser-playwright' import { Workers } from '../Workers' +import { AxiosRequestConfig } from 'axios' import { Counters, DashboardData } from '../../interface/DashboardData' import { GoogleSearch } from '../../interface/Search' -import { AxiosRequestConfig } from 'axios' type GoogleTrendsResponse = [ string, @@ -16,6 +16,10 @@ type GoogleTrendsResponse = [ ][] ]; +// Search stagnation thresholds (magic numbers extracted as constants) +const MOBILE_STAGNATION_LIMIT = 5 // Mobile searches: abort after 5 queries without points +const DESKTOP_STAGNATION_LIMIT = 10 // Desktop searches: abort after 10 queries without points + export class Search extends Workers { private bingHome = 'https://bing.com' private searchPageURL = '' @@ -119,15 +123,15 @@ export class Search extends Workers { if (missingPoints === 0) break - // Only for mobile searches - if (stagnation > 5 && this.bot.isMobile) { - this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn') + // Only for mobile searches - abort early if User-Agent is likely incorrect + if (stagnation > MOBILE_STAGNATION_LIMIT && this.bot.isMobile) { + this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${MOBILE_STAGNATION_LIMIT} iterations, likely bad User-Agent`, 'warn') break } - // If we didn't gain points for 10 iterations, assume it's stuck - if (stagnation > 10) { - this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') + // If we didn't gain points for many iterations, assume it's stuck + if (stagnation > DESKTOP_STAGNATION_LIMIT) { + this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${DESKTOP_STAGNATION_LIMIT} iterations, aborting searches`, 'warn') stagnation = 0 // allow fallback loop below break } diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 685fa4f..45ab3a1 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -139,6 +139,17 @@ function determineColorFromContent(content: string): number { return DISCORD.COLOR_GRAY } +/** + * Type guard to check if config has valid logging configuration + */ +function hasValidLogging(config: unknown): config is { logging: { excludeFunc?: string[]; webhookExcludeFunc?: string[] } } { + return typeof config === 'object' && + config !== null && + 'logging' in config && + typeof config.logging === 'object' && + config.logging !== null +} + function enqueueWebhookLog(url: string, line: string) { const buf = getBuffer(url) buf.lines.push(line) @@ -164,9 +175,8 @@ function enqueueWebhookLog(url: string, line: string) { export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void { const configData = loadConfig() - // Access logging config with fallback for backward compatibility - const configAny = configData as unknown as Record - const logging = configAny.logging as { excludeFunc?: string[]; logExcludeFunc?: string[] } | undefined + // Access logging config with type guard for safer access + const logging = hasValidLogging(configData) ? configData.logging : undefined const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? [] if (logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) { @@ -178,7 +188,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string, // Clean string for notifications (no chalk, structured) type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean } - const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg + const loggingCfg: LoggingCfg = logging || {} const shouldRedact = !!loggingCfg.redactEmails const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => { const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}` @@ -269,7 +279,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string, // Webhook streaming (live logs) try { - const loggingCfg: Record = (configAny.logging || {}) as Record + const loggingCfg: Record = (logging || {}) as Record const webhookCfg = configData.webhook const liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : '' const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '') diff --git a/src/util/Utils.ts b/src/util/Utils.ts index 2b95720..6245c10 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -123,6 +123,11 @@ export class Util { return [] } + // Check for undefined/null elements which could cause issues downstream + if (arr.some(item => item === undefined || item === null)) { + throw new Error('Array contains undefined or null elements which are not allowed.') + } + if (!Number.isFinite(numChunks) || numChunks <= 0) { throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`) }