feat: Centralize timeout constants and improve navigation error handling in Login flow

This commit is contained in:
2025-11-09 18:58:25 +01:00
parent 123b2f76b8
commit 5f9aafdd0f
4 changed files with 129 additions and 375 deletions

View File

@@ -1,99 +0,0 @@
# 🎯 Account Creator - How to Use
## ⚠️ CRITICAL: The `--` separator
**npm consumes `-y` if you don't use `--` !**
### ❌ WRONG (doesn't work)
```powershell
npm run creator "URL" -y
# Result: -y is consumed by npm, not passed to the script
```
### ✅ CORRECT (works)
```powershell
npm run creator -- "URL" -y
# The -- tells npm: "everything after belongs to the script"
```
---
## 📋 Examples
### Auto mode (no questions asked)
```powershell
npm run creator -- "https://rewards.bing.com/welcome?rh=E3DCB441" -y
```
### Auto mode with recovery email
```powershell
npm run creator -- "https://rewards.bing.com/welcome?rh=E3DCB441" -y maxou.freq@gmail.com
```
### Interactive mode (asks for options)
```powershell
npm run creator -- "https://rewards.bing.com/welcome?rh=E3DCB441"
```
### -y can be BEFORE the URL too
```powershell
npm run creator -- -y "https://rewards.bing.com/welcome?rh=E3DCB441"
```
---
## 🔧 Flags
| Flag | Description |
|------|-------------|
| `--` | **REQUIRED** - Separates npm args from script args |
| `-y` or `--yes` | Auto-accept all prompts (no questions) |
| `"URL"` | Referral URL (use quotes!) |
| `email@domain.com` | Recovery email (optional) |
---
## 🚨 Common Errors
### "Generate email automatically? (Y/n):" appears even with -y
**Cause**: You forgot the `--` separator
**Fix**: Add `--` after `npm run creator`
```powershell
# ❌ WRONG
npm run creator "URL" -y
# ✅ CORRECT
npm run creator -- "URL" -y
```
---
### URL is truncated at & character
**Cause**: URL not wrapped in quotes
**Fix**: Always use quotes around URLs
```powershell
# ❌ WRONG
npm run creator -- https://rewards.bing.com/welcome?rh=CODE&ref=xxx -y
# ✅ CORRECT
npm run creator -- "https://rewards.bing.com/welcome?rh=CODE&ref=xxx" -y
```
---
## 📝 Full Command Template
```powershell
npm run creator -- "https://rewards.bing.com/welcome?rh=YOUR_CODE" -y your.email@gmail.com
| | | |
| URL in quotes (required if contains &) | Optional recovery email
| |
-- separator (REQUIRED for -y to work) -y flag (auto mode)
```

View File

@@ -1,80 +0,0 @@
# 🐛 Automatic Error Reporting
## Overview
The bot automatically reports errors to a community webhook to help identify and fix issues faster, without requiring manual bug reports from users.
## Key Features
**Privacy-First** - Only non-sensitive error information is sent
**Opt-Out** - Can be disabled in configuration
**Obfuscated Webhook** - URL is base64-encoded
**Automatic Sanitization** - Removes emails, paths, tokens, and IPs
**Intelligent Filtering** - Excludes user configuration errors and false positives
**System Information** - Includes version, platform, architecture for debugging
## What's Sent
- Error message (sanitized)
- Stack trace (truncated, sanitized)
- Bot version
- Operating system and architecture
- Node.js version
- Timestamp
## What's NOT Sent
- ❌ Email addresses (redacted)
- ❌ File paths (redacted)
- ❌ IP addresses (redacted)
- ❌ Tokens/API keys (redacted)
- ❌ Account credentials
- ❌ Personal information
## Filtered Errors
The system intelligently filters out:
- User configuration errors (missing files, invalid credentials)
- Expected errors (no points, already completed)
- Network issues (proxy failures, port conflicts)
- Account-specific issues (banned accounts)
## Configuration
In `config.jsonc`:
```jsonc
"errorReporting": {
"enabled": true, // Set to false to disable
"webhookUrl": "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw=="
}
```
### Disable Error Reporting
Set `enabled` to `false`:
```jsonc
"errorReporting": {
"enabled": false,
"webhookUrl": "..."
}
```
## Privacy & Security
- No PII is sent
- All sensitive data is automatically redacted
- Webhook URL is obfuscated (base64)
- Fire-and-forget (never blocks execution)
- Silent failure if webhook is unreachable
## Technical Details
- **File**: `src/util/ErrorReportingWebhook.ts`
- **Integration**: Automatic via `Logger.ts`
- **Method**: HTTP POST to Discord webhook
- **Timeout**: 10 seconds
- **Filtering**: Pattern-based false positive detection
Thank you for helping improve the bot! 🙏

View File

@@ -3,7 +3,6 @@ import * as crypto from 'crypto'
import type { Locator, Page } from 'playwright'
import readline from 'readline'
import { TIMEOUTS } from '../constants'
import { MicrosoftRewardsBot } from '../index'
import { OAuth } from '../interface/OAuth'
import { saveSessionData } from '../util/Load'
@@ -40,19 +39,32 @@ const SELECTORS = {
const LOGIN_TARGET = { host: 'rewards.bing.com', path: '/' }
// Centralized timeouts to replace magic numbers throughout the file
const DEFAULT_TIMEOUTS = {
loginMaxMs: (() => {
const val = Number(process.env.LOGIN_MAX_WAIT_MS || 180000)
// IMPROVED: Use isFinite instead of isNaN for consistency
return (!Number.isFinite(val) || val < 10000 || val > 600000) ? 180000 : val
})(),
short: 200,
medium: 800,
long: 1500,
veryLong: 2000,
extraLong: 3000,
oauthMaxMs: 180000,
portalWaitMs: 15000,
elementCheck: 100,
fastPoll: 500
fastPoll: 500,
emailFieldWait: 8000,
passwordFieldWait: 4000,
rewardsPortalCheck: 8000,
navigationTimeout: 30000,
navigationTimeoutLinux: 60000,
totpThrottle: 5000,
totpWait: 1200,
passkeyNoPromptLog: 10000,
twoFactorTimeout: 120000,
bingVerificationMaxIterations: 10,
bingVerificationMaxIterationsMobile: 8
} as const
// Security pattern bundle
@@ -93,6 +105,72 @@ export class Login {
this.cleanupCompromisedInterval()
}
/**
* Reusable navigation with retry logic and chrome-error recovery
* Eliminates duplicate navigation code throughout the file
*/
private async navigateWithRetry(
page: Page,
url: string,
context: string,
maxAttempts = 3
): Promise<{ success: boolean; recoveryUsed: boolean }> {
const isLinux = process.platform === 'linux'
const navigationTimeout = isLinux ? DEFAULT_TIMEOUTS.navigationTimeoutLinux : DEFAULT_TIMEOUTS.navigationTimeout
let navigationSucceeded = false
let recoveryUsed = false
let attempts = 0
while (!navigationSucceeded && attempts < maxAttempts) {
attempts++
try {
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: navigationTimeout
})
navigationSucceeded = true
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
// Chrome-error recovery pattern
if (errorMsg.includes('chrome-error://chromewebdata/')) {
this.bot.log(this.bot.isMobile, context, `Navigation interrupted by chrome-error (attempt ${attempts}/${maxAttempts}), attempting recovery...`, 'warn')
await this.bot.utils.wait(DEFAULT_TIMEOUTS.long)
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
recoveryUsed = true
this.bot.log(this.bot.isMobile, context, '✓ Recovery successful via reload')
} catch (reloadError) {
if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, context, `Reload failed (attempt ${attempts}/${maxAttempts}), trying fresh navigation...`, 'warn')
await this.bot.utils.wait(DEFAULT_TIMEOUTS.veryLong)
} else {
throw reloadError
}
}
} else if (errorMsg.includes('ERR_PROXY_CONNECTION_FAILED') || errorMsg.includes('ERR_TUNNEL_CONNECTION_FAILED')) {
this.bot.log(this.bot.isMobile, context, `Proxy connection failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
if (attempts < maxAttempts) {
await this.bot.utils.wait(DEFAULT_TIMEOUTS.extraLong * attempts)
} else {
throw new Error(`Proxy connection failed for ${context} - check proxy configuration`)
}
} else if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, context, `Navigation failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
await this.bot.utils.wait(DEFAULT_TIMEOUTS.veryLong * attempts)
} else {
throw error
}
}
}
return { success: navigationSucceeded, recoveryUsed }
}
// --------------- Public API ---------------
async login(page: Page, email: string, password: string, totpSecret?: string) {
try {
@@ -115,58 +193,12 @@ export class Login {
return
}
const isLinux = process.platform === 'linux'
const navigationTimeout = isLinux ? 60000 : 30000
// IMPROVEMENT: Try initial navigation with better error handling
let navigationSucceeded = false
let recoveryUsed = false
let attempts = 0
const maxAttempts = 3
while (!navigationSucceeded && attempts < maxAttempts) {
attempts++
try {
await page.goto('https://www.bing.com/rewards/dashboard', {
waitUntil: 'domcontentloaded',
timeout: navigationTimeout
})
navigationSucceeded = true
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
// If interrupted by chrome-error, retry with reload approach
if (errorMsg.includes('chrome-error://chromewebdata/')) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Navigation interrupted by chrome-error (attempt ${attempts}/${maxAttempts}), attempting recovery...`, 'warn')
// Wait a bit for page to settle
await this.bot.utils.wait(1500) // Increased from 1000ms
// Try reload which usually fixes the issue
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
recoveryUsed = true
this.bot.log(this.bot.isMobile, 'LOGIN', '✓ Recovery successful via reload')
} catch (reloadError) {
// Last resort: try goto again
if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Reload failed (attempt ${attempts}/${maxAttempts}), trying fresh navigation...`, 'warn')
await this.bot.utils.wait(2000) // Increased from 1500ms
} else {
throw reloadError // Exhausted attempts
}
}
} else if (attempts < maxAttempts) {
// Different error, retry with backoff
this.bot.log(this.bot.isMobile, 'LOGIN', `Navigation failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
await this.bot.utils.wait(2000 * attempts) // Exponential backoff
} else {
// Exhausted attempts, rethrow
throw error
}
}
}
// IMPROVEMENT: Use centralized navigation retry logic
const { success: navigationSucceeded, recoveryUsed } = await this.navigateWithRetry(
page,
'https://www.bing.com/rewards/dashboard',
'LOGIN'
)
if (!navigationSucceeded) {
throw new Error('Failed to navigate to dashboard after multiple attempts')
@@ -174,7 +206,7 @@ export class Login {
// Only check for HTTP 400 if recovery was NOT used (to avoid double reload)
if (!recoveryUsed) {
await this.bot.utils.wait(500)
await this.bot.utils.wait(DEFAULT_TIMEOUTS.fastPoll)
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
@@ -182,8 +214,10 @@ export class Login {
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 detected in content, reloading...', 'warn')
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
await this.bot.utils.wait(1000)
const isLinux = process.platform === 'linux'
const timeout = isLinux ? DEFAULT_TIMEOUTS.navigationTimeoutLinux : DEFAULT_TIMEOUTS.navigationTimeout
await page.reload({ waitUntil: 'domcontentloaded', timeout })
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
}
}
@@ -247,61 +281,19 @@ export class Login {
url.searchParams.set('access_type', 'offline_access')
url.searchParams.set('login_hint', email)
const isLinux = process.platform === 'linux'
const navigationTimeout = isLinux ? 60000 : 30000
let navigationSucceeded = false
let recoveryUsed = false
let attempts = 0
const maxAttempts = 3
while (!navigationSucceeded && attempts < maxAttempts) {
attempts++
try {
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
if (errorMsg.includes('chrome-error://chromewebdata/')) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `OAuth navigation interrupted by chrome-error (attempt ${attempts}/${maxAttempts}), recovering...`, 'warn')
await this.bot.utils.wait(1500)
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
recoveryUsed = true
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'OAuth recovery successful')
} catch (reloadError) {
if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Reload failed (attempt ${attempts}/${maxAttempts}), retrying...`, 'warn')
await this.bot.utils.wait(2000)
} else {
throw reloadError
}
}
} else if (errorMsg.includes('ERR_PROXY_CONNECTION_FAILED') || errorMsg.includes('ERR_TUNNEL_CONNECTION_FAILED')) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Proxy connection failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
if (attempts < maxAttempts) {
await this.bot.utils.wait(3000 * attempts)
} else {
throw new Error('Proxy connection failed for OAuth - check proxy configuration')
}
} else if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Navigation failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
await this.bot.utils.wait(2000 * attempts)
} else {
throw error
}
}
}
// Use centralized navigation retry logic
const { success: navigationSucceeded, recoveryUsed } = await this.navigateWithRetry(
page,
url.href,
'LOGIN-APP'
)
if (!navigationSucceeded) {
throw new Error('Failed to navigate to OAuth page after multiple attempts')
}
if (!recoveryUsed) {
await this.bot.utils.wait(500)
await this.bot.utils.wait(DEFAULT_TIMEOUTS.fastPoll)
const content = await page.content().catch((err) => {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Failed to get page content for HTTP 400 check: ${err}`, 'warn')
return ''
@@ -312,8 +304,10 @@ export class Login {
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'HTTP 400 detected, reloading...', 'warn')
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
await this.bot.utils.wait(1000)
const isLinux = process.platform === 'linux'
const timeout = isLinux ? DEFAULT_TIMEOUTS.navigationTimeoutLinux : DEFAULT_TIMEOUTS.navigationTimeout
await page.reload({ waitUntil: 'domcontentloaded', timeout })
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
}
}
const start = Date.now()
@@ -410,35 +404,12 @@ export class Login {
private async tryReuseExistingSession(page: Page): Promise<boolean> {
const homeUrl = 'https://rewards.bing.com/'
try {
const isLinux = process.platform === 'linux'
const navigationTimeout = isLinux ? 60000 : 30000
// Try navigation with error recovery
let navigationSucceeded = false
let recoveryUsed = false
try {
await page.goto(homeUrl, { timeout: navigationTimeout })
navigationSucceeded = true
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
if (errorMsg.includes('chrome-error://chromewebdata/')) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Session check interrupted, recovering...', 'warn')
await this.bot.utils.wait(1000)
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
recoveryUsed = true
} catch (reloadError) {
await this.bot.utils.wait(1500)
await page.goto(homeUrl, { timeout: navigationTimeout })
navigationSucceeded = true
}
} else {
throw error
}
}
// Use centralized navigation retry logic
const { success: navigationSucceeded, recoveryUsed } = await this.navigateWithRetry(
page,
homeUrl,
'LOGIN'
)
if (!navigationSucceeded) return false
@@ -446,7 +417,7 @@ export class Login {
// Only check HTTP 400 if recovery was NOT used
if (!recoveryUsed) {
await this.bot.utils.wait(500)
await this.bot.utils.wait(DEFAULT_TIMEOUTS.fastPoll)
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
@@ -454,8 +425,10 @@ export class Login {
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 on session check, reloading...', 'warn')
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
await this.bot.utils.wait(1000)
const isLinux = process.platform === 'linux'
const timeout = isLinux ? DEFAULT_TIMEOUTS.navigationTimeoutLinux : DEFAULT_TIMEOUTS.navigationTimeout
await page.reload({ waitUntil: 'domcontentloaded', timeout })
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
}
}
await this.bot.browser.utils.reloadBadPage(page)
@@ -1244,63 +1217,21 @@ export class Login {
try {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing auth context')
const isLinux = process.platform === 'linux'
const navigationTimeout = isLinux ? 60000 : 30000
const verificationUrl = 'https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F'
let navigationSucceeded = false
let attempts = 0
const maxAttempts = 3
while (!navigationSucceeded && attempts < maxAttempts) {
attempts++
try {
await page.goto(verificationUrl, {
waitUntil: 'domcontentloaded',
timeout: navigationTimeout
})
navigationSucceeded = true
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
if (errorMsg.includes('chrome-error://chromewebdata/')) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', `Bing verification interrupted by chrome-error (attempt ${attempts}/${maxAttempts}), recovering...`, 'warn')
await this.bot.utils.wait(1500)
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
navigationSucceeded = true
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification recovery successful')
} catch (reloadError) {
if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', `Reload failed (attempt ${attempts}/${maxAttempts}), retrying navigation...`, 'warn')
await this.bot.utils.wait(2000)
} else {
throw reloadError
}
}
} else if (errorMsg.includes('ERR_PROXY_CONNECTION_FAILED') || errorMsg.includes('ERR_TUNNEL_CONNECTION_FAILED')) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', `Proxy connection failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
if (attempts < maxAttempts) {
await this.bot.utils.wait(3000 * attempts)
} else {
throw new Error('Proxy connection failed for Bing verification - check proxy configuration')
}
} else if (attempts < maxAttempts) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', `Navigation failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
await this.bot.utils.wait(2000 * attempts)
} else {
throw error
}
}
}
// Use centralized navigation retry logic
const { success: navigationSucceeded } = await this.navigateWithRetry(
page,
verificationUrl,
'LOGIN-BING'
)
if (!navigationSucceeded) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification navigation failed after multiple attempts', 'warn')
return
}
await this.bot.utils.wait(800)
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
@@ -1308,11 +1239,13 @@ export class Login {
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'HTTP 400 detected during Bing verification, reloading...', 'warn')
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout }).catch(logError('LOGIN-BING', 'Reload after HTTP 400 failed', this.bot.isMobile))
await this.bot.utils.wait(1000)
const isLinux = process.platform === 'linux'
const timeout = isLinux ? DEFAULT_TIMEOUTS.navigationTimeoutLinux : DEFAULT_TIMEOUTS.navigationTimeout
await page.reload({ waitUntil: 'domcontentloaded', timeout }).catch(logError('LOGIN-BING', 'Reload after HTTP 400 failed', this.bot.isMobile))
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
}
const maxIterations = this.bot.isMobile ? 8 : 10
const maxIterations = this.bot.isMobile ? DEFAULT_TIMEOUTS.bingVerificationMaxIterationsMobile : DEFAULT_TIMEOUTS.bingVerificationMaxIterations
for (let i = 0; i < maxIterations; i++) {
const u = new URL(page.url())
@@ -1700,7 +1633,6 @@ export class Login {
clearInterval(this.compromisedInterval)
this.compromisedInterval = undefined
}
// IMPROVED: Using centralized constant instead of magic number (5*60*1000)
this.compromisedInterval = setInterval(()=>{
try {
this.bot.log(this.bot.isMobile,'SECURITY','Security standby active. Manual review required before proceeding.','warn')
@@ -1708,7 +1640,7 @@ export class Login {
// Intentionally silent: If logging fails in interval, don't crash the timer
// The interval will try again in 5 minutes
}
}, TIMEOUTS.FIVE_MINUTES)
}, 300000) // 5 minutes = 300000ms
}
private cleanupCompromisedInterval() {

View File

@@ -308,7 +308,8 @@ export class Search extends Workers {
const trendsData = this.extractJsonFromResponse(rawText)
if (!trendsData) {
throw this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
throw new Error('Failed to parse Google Trends response')
}
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])