mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 01:36:16 +00:00
refactor: remove BanPredictor, ConfigValidator, RiskManager, SecurityDetector, and related utility files
- Deleted BanPredictor.ts, ConfigValidator.ts, RiskManager.ts, SecurityDetector.ts, and their associated interfaces and methods. - Removed all related logic for ban prediction, configuration validation, risk management, and security detection. - Added a new utility function to retrieve error messages from unknown error types.
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { dashboardState } from './state'
|
import { dashboardState } from './state'
|
||||||
import type { MicrosoftRewardsBot } from '../index'
|
import type { MicrosoftRewardsBot } from '../index'
|
||||||
|
import { getErrorMessage } from '../util/Utils'
|
||||||
|
|
||||||
export class BotController {
|
export class BotController {
|
||||||
private botInstance: MicrosoftRewardsBot | null = null
|
private botInstance: MicrosoftRewardsBot | null = null
|
||||||
private botPromise: Promise<void> | null = null
|
|
||||||
private startTime?: Date
|
private startTime?: Date
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -37,7 +37,8 @@ export class BotController {
|
|||||||
dashboardState.setRunning(true)
|
dashboardState.setRunning(true)
|
||||||
dashboardState.setBotInstance(this.botInstance)
|
dashboardState.setBotInstance(this.botInstance)
|
||||||
|
|
||||||
this.botPromise = (async () => {
|
// Run bot asynchronously - don't block the API response
|
||||||
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
this.log('✓ Bot initialized, starting execution...', 'log')
|
this.log('✓ Bot initialized, starting execution...', 'log')
|
||||||
|
|
||||||
@@ -46,22 +47,16 @@ export class BotController {
|
|||||||
|
|
||||||
this.log('✓ Bot completed successfully', 'log')
|
this.log('✓ Bot completed successfully', 'log')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
this.log(`Bot error: ${getErrorMessage(error)}`, 'error')
|
||||||
this.log(`Bot error: ${errorMsg}`, 'error')
|
|
||||||
throw error
|
|
||||||
} finally {
|
} finally {
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
this.botPromise.catch(error => {
|
|
||||||
this.log(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
||||||
})
|
|
||||||
|
|
||||||
return { success: true, pid: process.pid }
|
return { success: true, pid: process.pid }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
const errorMsg = getErrorMessage(error)
|
||||||
this.log(`Failed to start bot: ${errorMsg}`, 'error')
|
this.log(`Failed to start bot: ${errorMsg}`, 'error')
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
@@ -81,7 +76,7 @@ export class BotController {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
const errorMsg = getErrorMessage(error)
|
||||||
this.log(`Error stopping bot: ${errorMsg}`, 'error')
|
this.log(`Error stopping bot: ${errorMsg}`, 'error')
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
@@ -90,14 +85,19 @@ export class BotController {
|
|||||||
|
|
||||||
public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||||
this.log('🔄 Restarting bot...', 'log')
|
this.log('🔄 Restarting bot...', 'log')
|
||||||
this.stop()
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
const stopResult = this.stop()
|
||||||
setTimeout(async () => {
|
if (!stopResult.success && stopResult.error !== 'Bot is not running') {
|
||||||
const result = await this.start()
|
return { success: false, error: `Failed to stop: ${stopResult.error}` }
|
||||||
resolve(result)
|
}
|
||||||
}, 2000)
|
|
||||||
})
|
await this.wait(2000)
|
||||||
|
|
||||||
|
return await this.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async wait(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStatus(): {
|
public getStatus(): {
|
||||||
@@ -116,7 +116,6 @@ export class BotController {
|
|||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
this.botInstance = null
|
this.botInstance = null
|
||||||
this.botPromise = null
|
|
||||||
this.startTime = undefined
|
this.startTime = undefined
|
||||||
dashboardState.setRunning(false)
|
dashboardState.setRunning(false)
|
||||||
dashboardState.setBotInstance(undefined)
|
dashboardState.setBotInstance(undefined)
|
||||||
|
|||||||
@@ -7,46 +7,39 @@ import { botController } from './BotController'
|
|||||||
|
|
||||||
export const apiRouter = Router()
|
export const apiRouter = Router()
|
||||||
|
|
||||||
|
// Helper to extract error message
|
||||||
|
const getErr = (e: unknown): string => e instanceof Error ? e.message : 'Unknown error'
|
||||||
|
|
||||||
|
// Helper to load accounts if not already loaded
|
||||||
|
function ensureAccountsLoaded(): void {
|
||||||
|
const accounts = dashboardState.getAccounts()
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
try {
|
||||||
|
const loadedAccounts = loadAccounts()
|
||||||
|
dashboardState.initializeAccounts(loadedAccounts.map(a => a.email))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Dashboard] Failed to load accounts:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/status - Bot status
|
// GET /api/status - Bot status
|
||||||
apiRouter.get('/status', (_req: Request, res: Response) => {
|
apiRouter.get('/status', (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const accounts = dashboardState.getAccounts()
|
ensureAccountsLoaded()
|
||||||
|
|
||||||
// If no accounts loaded yet, try to load them
|
|
||||||
if (accounts.length === 0) {
|
|
||||||
try {
|
|
||||||
const loadedAccounts = loadAccounts()
|
|
||||||
dashboardState.initializeAccounts(loadedAccounts.map(a => a.email))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Dashboard] Failed to load accounts for status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(dashboardState.getStatus())
|
res.json(dashboardState.getStatus())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET /api/accounts - List all accounts with masked emails
|
// GET /api/accounts - List all accounts with masked emails
|
||||||
apiRouter.get('/accounts', (_req: Request, res: Response) => {
|
apiRouter.get('/accounts', (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
let accounts = dashboardState.getAccounts()
|
ensureAccountsLoaded()
|
||||||
|
res.json(dashboardState.getAccounts())
|
||||||
// If no accounts in state, try to load from config
|
|
||||||
if (accounts.length === 0) {
|
|
||||||
try {
|
|
||||||
const loadedAccounts = loadAccounts()
|
|
||||||
dashboardState.initializeAccounts(loadedAccounts.map(a => a.email))
|
|
||||||
accounts = dashboardState.getAccounts()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Dashboard] Failed to load accounts:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(accounts)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -57,7 +50,7 @@ apiRouter.get('/logs', (req: Request, res: Response) => {
|
|||||||
const logs = dashboardState.getLogs(Math.min(limit, 500))
|
const logs = dashboardState.getLogs(Math.min(limit, 500))
|
||||||
res.json(logs)
|
res.json(logs)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -67,7 +60,7 @@ apiRouter.delete('/logs', (_req: Request, res: Response) => {
|
|||||||
dashboardState.clearLogs()
|
dashboardState.clearLogs()
|
||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -98,7 +91,7 @@ apiRouter.get('/history', (_req: Request, res: Response): void => {
|
|||||||
|
|
||||||
res.json(summaries.slice(0, 50))
|
res.json(summaries.slice(0, 50))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -115,7 +108,7 @@ apiRouter.get('/config', (_req: Request, res: Response) => {
|
|||||||
|
|
||||||
res.json(safe)
|
res.json(safe)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,7 +132,7 @@ apiRouter.post('/config', (req: Request, res: Response): void => {
|
|||||||
|
|
||||||
res.json({ success: true, backup: backupPath })
|
res.json({ success: true, backup: backupPath })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -148,26 +141,19 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> =>
|
|||||||
try {
|
try {
|
||||||
const status = botController.getStatus()
|
const status = botController.getStatus()
|
||||||
if (status.running) {
|
if (status.running) {
|
||||||
res.status(400).json({ error: 'Bot already running', pid: status.pid })
|
sendError(res, 400, `Bot already running (PID: ${status.pid})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await botController.start()
|
const result = await botController.start()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json({
|
sendSuccess(res, { message: 'Bot started successfully', pid: result.pid })
|
||||||
success: true,
|
|
||||||
message: 'Bot started successfully',
|
|
||||||
pid: result.pid
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
sendError(res, 500, result.error || 'Failed to start bot')
|
||||||
success: false,
|
|
||||||
error: result.error || 'Failed to start bot'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
sendError(res, 500, getErr(error))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -177,12 +163,12 @@ apiRouter.post('/stop', (_req: Request, res: Response): void => {
|
|||||||
const result = botController.stop()
|
const result = botController.stop()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json({ success: true, message: 'Bot stopped successfully' })
|
sendSuccess(res, { message: 'Bot stopped successfully' })
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ success: false, error: result.error || 'Failed to stop bot' })
|
sendError(res, 400, result.error || 'Failed to stop bot')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
sendError(res, 500, getErr(error))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -192,19 +178,12 @@ apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> =
|
|||||||
const result = await botController.restart()
|
const result = await botController.restart()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json({
|
sendSuccess(res, { message: 'Bot restarted successfully', pid: result.pid })
|
||||||
success: true,
|
|
||||||
message: 'Bot restarted successfully',
|
|
||||||
pid: result.pid
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
sendError(res, 500, result.error || 'Failed to restart bot')
|
||||||
success: false,
|
|
||||||
error: result.error || 'Failed to restart bot'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
sendError(res, 500, getErr(error))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -230,7 +209,7 @@ apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise<void
|
|||||||
suggestion: 'Use /api/restart endpoint to run all accounts'
|
suggestion: 'Use /api/restart endpoint to run all accounts'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -253,7 +232,7 @@ apiRouter.get('/metrics', (_req: Request, res: Response) => {
|
|||||||
accountsError: accounts.filter(a => a.status === 'error').length
|
accountsError: accounts.filter(a => a.status === 'error').length
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -275,7 +254,7 @@ apiRouter.get('/account/:email', (req: Request, res: Response): void => {
|
|||||||
|
|
||||||
res.json(account)
|
res.json(account)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -302,10 +281,11 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
|
|||||||
|
|
||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: getErr(error) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Helper to mask sensitive URLs
|
||||||
function maskUrl(url: string): string {
|
function maskUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url)
|
const parsed = new URL(url)
|
||||||
@@ -320,3 +300,13 @@ function maskUrl(url: string): string {
|
|||||||
return '***'
|
return '***'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to send error response
|
||||||
|
function sendError(res: Response, status: number, message: string): void {
|
||||||
|
res.status(status).json({ error: message })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to send success response
|
||||||
|
function sendSuccess(res: Response, data: Record<string, unknown>): void {
|
||||||
|
res.json({ success: true, ...data })
|
||||||
|
}
|
||||||
|
|||||||
112
src/index.ts
112
src/index.ts
@@ -15,8 +15,6 @@ import { loadAccounts, loadConfig, saveSessionData } from './util/Load'
|
|||||||
import Axios from './util/Axios'
|
import Axios from './util/Axios'
|
||||||
import Humanizer from './util/Humanizer'
|
import Humanizer from './util/Humanizer'
|
||||||
import { detectBanReason } from './util/BanDetector'
|
import { detectBanReason } from './util/BanDetector'
|
||||||
import { RiskManager, RiskMetrics, RiskEvent } from './util/RiskManager'
|
|
||||||
import { BanPredictor } from './util/BanPredictor'
|
|
||||||
import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
||||||
import JobState from './util/JobState'
|
import JobState from './util/JobState'
|
||||||
import { StartupValidator } from './util/StartupValidator'
|
import { StartupValidator } from './util/StartupValidator'
|
||||||
@@ -61,10 +59,6 @@ export class MicrosoftRewardsBot {
|
|||||||
private runId: string = Math.random().toString(36).slice(2)
|
private runId: string = Math.random().toString(36).slice(2)
|
||||||
private bannedTriggered: { email: string; reason: string } | null = null
|
private bannedTriggered: { email: string; reason: string } | null = null
|
||||||
private globalStandby: { active: boolean; reason?: string } = { active: false }
|
private globalStandby: { active: boolean; reason?: string } = { active: false }
|
||||||
private riskManager?: RiskManager
|
|
||||||
private lastRiskMetrics?: RiskMetrics
|
|
||||||
private riskThresholdTriggered: boolean = false
|
|
||||||
private banPredictor?: BanPredictor
|
|
||||||
private accountJobState?: JobState
|
private accountJobState?: JobState
|
||||||
private accountRunCounts: Map<string, number> = new Map()
|
private accountRunCounts: Map<string, number> = new Map()
|
||||||
|
|
||||||
@@ -95,13 +89,6 @@ export class MicrosoftRewardsBot {
|
|||||||
cacheMinutes: this.config.queryDiversity.cacheMinutes
|
cacheMinutes: this.config.queryDiversity.cacheMinutes
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.riskManagement?.enabled) {
|
|
||||||
this.riskManager = new RiskManager()
|
|
||||||
if (this.config.riskManagement.banPrediction) {
|
|
||||||
this.banPredictor = new BanPredictor(this.riskManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buy mode: CLI args take precedence over config
|
// Buy mode: CLI args take precedence over config
|
||||||
const idx = process.argv.indexOf('-buy')
|
const idx = process.argv.indexOf('-buy')
|
||||||
@@ -143,36 +130,6 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetRiskTracking(): void {
|
|
||||||
if (this.riskManager) {
|
|
||||||
this.riskManager.reset()
|
|
||||||
}
|
|
||||||
this.lastRiskMetrics = undefined
|
|
||||||
this.riskThresholdTriggered = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private recordRiskEvent(type: RiskEvent['type'], severity: number, context?: string): void {
|
|
||||||
if (!this.riskManager || this.config.riskManagement?.enabled !== true) return
|
|
||||||
this.riskManager.recordEvent(type, severity, context)
|
|
||||||
const metrics = this.riskManager.assessRisk()
|
|
||||||
this.lastRiskMetrics = metrics
|
|
||||||
|
|
||||||
const threshold = this.config.riskManagement?.riskThreshold
|
|
||||||
if (typeof threshold === 'number' && metrics.score >= threshold && !this.riskThresholdTriggered) {
|
|
||||||
this.riskThresholdTriggered = true
|
|
||||||
log('main', 'RISK', `Risk score ${metrics.score} exceeded threshold ${threshold} (level=${metrics.level})`, 'warn', 'yellow')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.riskManagement?.stopOnCritical && metrics.level === 'critical') {
|
|
||||||
void this.engageGlobalStandby('risk-critical', this.currentAccountEmail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRiskDelayMultiplier(): number {
|
|
||||||
if (!this.config.riskManagement?.enabled || this.config.riskManagement.autoAdjustDelays === false) return 1
|
|
||||||
return this.lastRiskMetrics?.delayMultiplier ?? 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldSkipAccount(email: string, dayKey: string): boolean {
|
private shouldSkipAccount(email: string, dayKey: string): boolean {
|
||||||
if (!this.accountJobState) return false
|
if (!this.accountJobState) return false
|
||||||
if (this.config.jobState?.skipCompletedAccounts === false) return false
|
if (this.config.jobState?.skipCompletedAccounts === false) return false
|
||||||
@@ -525,7 +482,6 @@ export class MicrosoftRewardsBot {
|
|||||||
// Reset compromised state per account
|
// Reset compromised state per account
|
||||||
this.compromisedModeActive = false
|
this.compromisedModeActive = false
|
||||||
this.compromisedReason = undefined
|
this.compromisedReason = undefined
|
||||||
this.resetRiskTracking()
|
|
||||||
|
|
||||||
// If humanization allowed windows are configured, wait until within a window
|
// If humanization allowed windows are configured, wait until within a window
|
||||||
try {
|
try {
|
||||||
@@ -568,9 +524,7 @@ export class MicrosoftRewardsBot {
|
|||||||
initialTotal: 0,
|
initialTotal: 0,
|
||||||
endTotal: 0,
|
endTotal: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
banned,
|
banned
|
||||||
riskScore: 0,
|
|
||||||
riskLevel: 'safe'
|
|
||||||
}
|
}
|
||||||
this.accountSummaries.push(summary)
|
this.accountSummaries.push(summary)
|
||||||
this.persistAccountCompletion(account.email, accountDayKey, summary)
|
this.persistAccountCompletion(account.email, accountDayKey, summary)
|
||||||
@@ -583,24 +537,20 @@ export class MicrosoftRewardsBot {
|
|||||||
// Run both and capture results with detailed logging
|
// Run both and capture results with detailed logging
|
||||||
const desktopPromise = this.Desktop(account).catch((e: unknown) => {
|
const desktopPromise = this.Desktop(account).catch((e: unknown) => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
this.recordRiskEvent('error', 6, `desktop:${msg}`)
|
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullError('desktop', e, verbose)); return null
|
errors.push(formatFullError('desktop', e, verbose)); return null
|
||||||
})
|
})
|
||||||
const mobilePromise = mobileInstance.Mobile(account).catch((e: unknown) => {
|
const mobilePromise = mobileInstance.Mobile(account).catch((e: unknown) => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
this.recordRiskEvent('error', 6, `mobile:${msg}`)
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullError('mobile', e, verbose)); return null
|
errors.push(formatFullError('mobile', e, verbose)); return null
|
||||||
@@ -613,7 +563,6 @@ export class MicrosoftRewardsBot {
|
|||||||
desktopCollected = desktopResult.value.collectedPoints
|
desktopCollected = desktopResult.value.collectedPoints
|
||||||
} else if (desktopResult.status === 'rejected') {
|
} else if (desktopResult.status === 'rejected') {
|
||||||
log(false, 'TASK', `Desktop promise rejected unexpectedly: ${shortErr(desktopResult.reason)}`,'error')
|
log(false, 'TASK', `Desktop promise rejected unexpectedly: ${shortErr(desktopResult.reason)}`,'error')
|
||||||
this.recordRiskEvent('error', 6, `desktop-rejected:${shortErr(desktopResult.reason)}`)
|
|
||||||
errors.push(formatFullError('desktop-rejected', desktopResult.reason, verbose))
|
errors.push(formatFullError('desktop-rejected', desktopResult.reason, verbose))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +572,6 @@ export class MicrosoftRewardsBot {
|
|||||||
mobileCollected = mobileResult.value.collectedPoints
|
mobileCollected = mobileResult.value.collectedPoints
|
||||||
} else if (mobileResult.status === 'rejected') {
|
} else if (mobileResult.status === 'rejected') {
|
||||||
log(true, 'TASK', `Mobile promise rejected unexpectedly: ${shortErr(mobileResult.reason)}`,'error')
|
log(true, 'TASK', `Mobile promise rejected unexpectedly: ${shortErr(mobileResult.reason)}`,'error')
|
||||||
this.recordRiskEvent('error', 6, `mobile-rejected:${shortErr(mobileResult.reason)}`)
|
|
||||||
errors.push(formatFullError('mobile-rejected', mobileResult.reason, verbose))
|
errors.push(formatFullError('mobile-rejected', mobileResult.reason, verbose))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -631,12 +579,10 @@ export class MicrosoftRewardsBot {
|
|||||||
this.isMobile = false
|
this.isMobile = false
|
||||||
const desktopResult = await this.Desktop(account).catch(e => {
|
const desktopResult = await this.Desktop(account).catch(e => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
this.recordRiskEvent('error', 6, `desktop:${msg}`)
|
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullError('desktop', e, verbose)); return null
|
errors.push(formatFullError('desktop', e, verbose)); return null
|
||||||
@@ -650,12 +596,10 @@ export class MicrosoftRewardsBot {
|
|||||||
this.isMobile = true
|
this.isMobile = true
|
||||||
const mobileResult = await this.Mobile(account).catch((e: unknown) => {
|
const mobileResult = await this.Mobile(account).catch((e: unknown) => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
this.recordRiskEvent('error', 6, `mobile:${msg}`)
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullError('mobile', e, verbose)); return null
|
errors.push(formatFullError('mobile', e, verbose)); return null
|
||||||
@@ -681,34 +625,6 @@ export class MicrosoftRewardsBot {
|
|||||||
: (desktopInitial || mobileInitial || 0)
|
: (desktopInitial || mobileInitial || 0)
|
||||||
|
|
||||||
const endTotal = initialTotal + totalCollected
|
const endTotal = initialTotal + totalCollected
|
||||||
if (!banned.status) {
|
|
||||||
this.recordRiskEvent('success', 1, 'account-complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
const riskMetrics = this.lastRiskMetrics
|
|
||||||
let riskScore = riskMetrics?.score
|
|
||||||
const riskLevel = riskMetrics?.level
|
|
||||||
let banPredictionScore: number | undefined
|
|
||||||
let banLikelihood: string | undefined
|
|
||||||
|
|
||||||
if (this.banPredictor && this.config.riskManagement?.banPrediction) {
|
|
||||||
const prediction = this.banPredictor.predictBanRisk(account.email, runNumber, runNumber)
|
|
||||||
banPredictionScore = prediction.riskScore
|
|
||||||
banLikelihood = prediction.likelihood
|
|
||||||
riskScore = prediction.riskScore
|
|
||||||
if (prediction.likelihood === 'high' || prediction.likelihood === 'critical') {
|
|
||||||
log('main', 'RISK', `Ban predictor warning for ${account.email}: likelihood=${prediction.likelihood} score=${prediction.riskScore}`, 'warn', 'yellow')
|
|
||||||
}
|
|
||||||
if (banned.status) {
|
|
||||||
this.banPredictor.recordBan(account.email, runNumber, runNumber)
|
|
||||||
} else {
|
|
||||||
this.banPredictor.recordSuccess(account.email, runNumber, runNumber)
|
|
||||||
}
|
|
||||||
} else if (banned.status && this.banPredictor) {
|
|
||||||
this.banPredictor.recordBan(account.email, runNumber, runNumber)
|
|
||||||
} else if (this.banPredictor && !banned.status) {
|
|
||||||
this.banPredictor.recordSuccess(account.email, runNumber, runNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary: AccountSummary = {
|
const summary: AccountSummary = {
|
||||||
email: account.email,
|
email: account.email,
|
||||||
@@ -719,11 +635,7 @@ export class MicrosoftRewardsBot {
|
|||||||
initialTotal,
|
initialTotal,
|
||||||
endTotal,
|
endTotal,
|
||||||
errors,
|
errors,
|
||||||
banned,
|
banned
|
||||||
riskScore,
|
|
||||||
riskLevel,
|
|
||||||
banPredictionScore,
|
|
||||||
banLikelihood
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accountSummaries.push(summary)
|
this.accountSummaries.push(summary)
|
||||||
@@ -733,7 +645,6 @@ export class MicrosoftRewardsBot {
|
|||||||
this.bannedTriggered = { email: account.email, reason: banned.reason }
|
this.bannedTriggered = { email: account.email, reason: banned.reason }
|
||||||
// Enter global standby: do not proceed to next accounts
|
// Enter global standby: do not proceed to next accounts
|
||||||
this.globalStandby = { active: true, reason: `banned:${banned.reason}` }
|
this.globalStandby = { active: true, reason: `banned:${banned.reason}` }
|
||||||
this.recordRiskEvent('ban_hint', 9, `final-ban:${banned.reason}`)
|
|
||||||
await this.sendGlobalSecurityStandbyAlert(account.email, `Ban detected: ${banned.reason || 'unknown'}`)
|
await this.sendGlobalSecurityStandbyAlert(account.email, `Ban detected: ${banned.reason || 'unknown'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1119,14 +1030,6 @@ export class MicrosoftRewardsBot {
|
|||||||
const extras: string[] = []
|
const extras: string[] = []
|
||||||
if (summary.banned?.status) extras.push(`BAN:${summary.banned.reason || 'detected'}`)
|
if (summary.banned?.status) extras.push(`BAN:${summary.banned.reason || 'detected'}`)
|
||||||
if (summary.errors.length) extras.push(`ERR:${summary.errors.slice(0, 1).join(' | ')}`)
|
if (summary.errors.length) extras.push(`ERR:${summary.errors.slice(0, 1).join(' | ')}`)
|
||||||
if (summary.riskLevel) {
|
|
||||||
const scoreLabel = summary.riskScore != null ? `(${summary.riskScore})` : ''
|
|
||||||
extras.push(`RISK:${summary.riskLevel}${scoreLabel}`)
|
|
||||||
}
|
|
||||||
if (summary.banLikelihood) {
|
|
||||||
const predLabel = summary.banPredictionScore != null ? `(${summary.banPredictionScore})` : ''
|
|
||||||
extras.push(`PRED:${summary.banLikelihood}${predLabel}`)
|
|
||||||
}
|
|
||||||
const tail = extras.length ? ` | ${extras.join(' • ')}` : ''
|
const tail = extras.length ? ` | ${extras.join(' • ')}` : ''
|
||||||
return `${statusIcon} ${email} ${total} ${desktop}${mobile} ${totals} ${duration}${tail}`
|
return `${statusIcon} ${email} ${total} ${desktop}${mobile} ${totals} ${duration}${tail}`
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1054,6 @@ export class MicrosoftRewardsBot {
|
|||||||
const accountLines = summaries.map(buildAccountLine)
|
const accountLines = summaries.map(buildAccountLine)
|
||||||
const accountChunks = chunkLines(accountLines)
|
const accountChunks = chunkLines(accountLines)
|
||||||
|
|
||||||
const riskSamples = summaries.filter(s => typeof s.riskScore === 'number' && !Number.isNaN(s.riskScore as number))
|
|
||||||
const globalLines = [
|
const globalLines = [
|
||||||
`Total points: **${formatNumber(totalInitial)}** → **${formatNumber(totalEnd)}** (${formatSigned(totalCollected)} pts)`,
|
`Total points: **${formatNumber(totalInitial)}** → **${formatNumber(totalEnd)}** (${formatSigned(totalCollected)} pts)`,
|
||||||
`Accounts: ✅ ${successes}${accountsWithErrors > 0 ? ` • ⚠️ ${accountsWithErrors}` : ''} (${totalAccounts} total)`,
|
`Accounts: ✅ ${successes}${accountsWithErrors > 0 ? ` • ⚠️ ${accountsWithErrors}` : ''} (${totalAccounts} total)`,
|
||||||
@@ -1159,12 +1061,6 @@ export class MicrosoftRewardsBot {
|
|||||||
`Runtime: **${formatDuration(totalDuration)}**`
|
`Runtime: **${formatDuration(totalDuration)}**`
|
||||||
]
|
]
|
||||||
|
|
||||||
if (riskSamples.length > 0) {
|
|
||||||
const avgRiskScore = riskSamples.reduce((sum, s) => sum + (s.riskScore ?? 0), 0) / riskSamples.length
|
|
||||||
const highestRisk = riskSamples.reduce((prev, curr) => ((curr.riskScore ?? 0) > (prev.riskScore ?? 0) ? curr : prev))
|
|
||||||
globalLines.push(`Risk avg: **${avgRiskScore.toFixed(1)}** • Highest: ${highestRisk.email} (${highestRisk.riskScore ?? 0})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalStatsValue = globalLines.join('\n')
|
const globalStatsValue = globalLines.join('\n')
|
||||||
|
|
||||||
const fields: { name: string; value: string; inline?: boolean }[] = [
|
const fields: { name: string; value: string; inline?: boolean }[] = [
|
||||||
@@ -1273,10 +1169,6 @@ interface AccountSummary {
|
|||||||
endTotal: number
|
endTotal: number
|
||||||
errors: string[]
|
errors: string[]
|
||||||
banned?: { status: boolean; reason: string }
|
banned?: { status: boolean; reason: string }
|
||||||
riskScore?: number
|
|
||||||
riskLevel?: string
|
|
||||||
banPredictionScore?: number
|
|
||||||
banLikelihood?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortErr(e: unknown): string {
|
function shortErr(e: unknown): string {
|
||||||
|
|||||||
@@ -1,394 +0,0 @@
|
|||||||
import { RiskManager, RiskEvent } from './RiskManager'
|
|
||||||
|
|
||||||
export interface BanPattern {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
weight: number // 0-10
|
|
||||||
detected: boolean
|
|
||||||
evidence: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BanPrediction {
|
|
||||||
riskScore: number // 0-100
|
|
||||||
confidence: number // 0-1
|
|
||||||
likelihood: 'very-low' | 'low' | 'medium' | 'high' | 'critical'
|
|
||||||
patterns: BanPattern[]
|
|
||||||
recommendation: string
|
|
||||||
preventiveActions: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoricalData {
|
|
||||||
email: string
|
|
||||||
timestamp: number
|
|
||||||
banned: boolean
|
|
||||||
preBanEvents: RiskEvent[]
|
|
||||||
accountAge: number // days since first use
|
|
||||||
totalRuns: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BanPredictor uses machine-learning-style pattern analysis to predict ban risk.
|
|
||||||
* Learns from historical data and real-time signals to calculate ban probability.
|
|
||||||
*/
|
|
||||||
export class BanPredictor {
|
|
||||||
private riskManager: RiskManager
|
|
||||||
private history: HistoricalData[] = []
|
|
||||||
private patterns: BanPattern[] = []
|
|
||||||
|
|
||||||
constructor(riskManager: RiskManager) {
|
|
||||||
this.riskManager = riskManager
|
|
||||||
this.initializePatterns()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze current state and predict ban risk
|
|
||||||
*/
|
|
||||||
predictBanRisk(accountEmail: string, accountAgeDays: number, totalRuns: number): BanPrediction {
|
|
||||||
const riskMetrics = this.riskManager.assessRisk()
|
|
||||||
const recentEvents = this.riskManager.getRecentEvents(60)
|
|
||||||
|
|
||||||
// Detect patterns
|
|
||||||
this.detectPatterns(recentEvents, accountAgeDays, totalRuns)
|
|
||||||
|
|
||||||
// Calculate base risk from RiskManager
|
|
||||||
const baseRisk = riskMetrics.score
|
|
||||||
|
|
||||||
// Apply ML-style feature weights
|
|
||||||
const featureScore = this.calculateFeatureScore(recentEvents, accountAgeDays, totalRuns)
|
|
||||||
|
|
||||||
// Pattern detection bonus
|
|
||||||
const detectedPatterns = this.patterns.filter(p => p.detected)
|
|
||||||
const patternPenalty = detectedPatterns.reduce((sum, p) => sum + p.weight, 0)
|
|
||||||
|
|
||||||
// Historical learning adjustment
|
|
||||||
const historicalAdjustment = this.getHistoricalAdjustment(accountEmail)
|
|
||||||
|
|
||||||
// Final risk score (capped at 100)
|
|
||||||
const finalScore = Math.min(100, baseRisk + featureScore + patternPenalty + historicalAdjustment)
|
|
||||||
|
|
||||||
// Calculate confidence (based on data availability)
|
|
||||||
const confidence = this.calculateConfidence(recentEvents.length, this.history.length)
|
|
||||||
|
|
||||||
// Determine likelihood tier
|
|
||||||
let likelihood: BanPrediction['likelihood']
|
|
||||||
if (finalScore < 20) likelihood = 'very-low'
|
|
||||||
else if (finalScore < 40) likelihood = 'low'
|
|
||||||
else if (finalScore < 60) likelihood = 'medium'
|
|
||||||
else if (finalScore < 80) likelihood = 'high'
|
|
||||||
else likelihood = 'critical'
|
|
||||||
|
|
||||||
// Generate recommendations
|
|
||||||
const recommendation = this.generateRecommendation(finalScore)
|
|
||||||
const preventiveActions = this.generatePreventiveActions(detectedPatterns)
|
|
||||||
|
|
||||||
return {
|
|
||||||
riskScore: Math.round(finalScore),
|
|
||||||
confidence: Number(confidence.toFixed(2)),
|
|
||||||
likelihood,
|
|
||||||
patterns: detectedPatterns,
|
|
||||||
recommendation,
|
|
||||||
preventiveActions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record ban event for learning
|
|
||||||
*/
|
|
||||||
recordBan(email: string, accountAgeDays: number, totalRuns: number): void {
|
|
||||||
const preBanEvents = this.riskManager.getRecentEvents(120)
|
|
||||||
|
|
||||||
this.history.push({
|
|
||||||
email,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
banned: true,
|
|
||||||
preBanEvents,
|
|
||||||
accountAge: accountAgeDays,
|
|
||||||
totalRuns
|
|
||||||
})
|
|
||||||
|
|
||||||
// Keep history limited (last 100 bans)
|
|
||||||
if (this.history.length > 100) {
|
|
||||||
this.history.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record successful run (no ban) for learning
|
|
||||||
*/
|
|
||||||
recordSuccess(email: string, accountAgeDays: number, totalRuns: number): void {
|
|
||||||
this.history.push({
|
|
||||||
email,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
banned: false,
|
|
||||||
preBanEvents: [],
|
|
||||||
accountAge: accountAgeDays,
|
|
||||||
totalRuns
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.history.length > 100) {
|
|
||||||
this.history.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize known ban patterns
|
|
||||||
*/
|
|
||||||
private initializePatterns(): void {
|
|
||||||
this.patterns = [
|
|
||||||
{
|
|
||||||
name: 'rapid-captcha-sequence',
|
|
||||||
description: 'Multiple captchas in short timespan',
|
|
||||||
weight: 8,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'high-error-rate',
|
|
||||||
description: 'Excessive errors (>50% in last hour)',
|
|
||||||
weight: 6,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'timeout-storm',
|
|
||||||
description: 'Many consecutive timeouts',
|
|
||||||
weight: 7,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'suspicious-timing',
|
|
||||||
description: 'Activity at unusual hours or too consistent',
|
|
||||||
weight: 5,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'new-account-aggressive',
|
|
||||||
description: 'Aggressive activity on young account',
|
|
||||||
weight: 9,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'proxy-flagged',
|
|
||||||
description: 'Proxy showing signs of blacklisting',
|
|
||||||
weight: 7,
|
|
||||||
detected: false,
|
|
||||||
evidence: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect patterns in recent events
|
|
||||||
*/
|
|
||||||
private detectPatterns(events: RiskEvent[], accountAgeDays: number, totalRuns: number): void {
|
|
||||||
// Reset detection
|
|
||||||
for (const p of this.patterns) {
|
|
||||||
p.detected = false
|
|
||||||
p.evidence = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const captchaEvents = events.filter(e => e.type === 'captcha')
|
|
||||||
const errorEvents = events.filter(e => e.type === 'error')
|
|
||||||
const timeoutEvents = events.filter(e => e.type === 'timeout')
|
|
||||||
|
|
||||||
// Pattern 1: Rapid captcha sequence
|
|
||||||
if (captchaEvents.length >= 3) {
|
|
||||||
const timeSpan = (events[events.length - 1]?.timestamp || 0) - (events[0]?.timestamp || 0)
|
|
||||||
if (timeSpan < 1800000) { // 30 min
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'rapid-captcha-sequence')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push(`${captchaEvents.length} captchas in ${Math.round(timeSpan / 60000)}min`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 2: High error rate
|
|
||||||
const errorRate = errorEvents.length / Math.max(1, events.length)
|
|
||||||
if (errorRate > 0.5) {
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'high-error-rate')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push(`Error rate: ${(errorRate * 100).toFixed(1)}%`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 3: Timeout storm
|
|
||||||
if (timeoutEvents.length >= 5) {
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'timeout-storm')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push(`${timeoutEvents.length} timeouts detected`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 4: Suspicious timing (all events within same hour)
|
|
||||||
if (events.length > 5) {
|
|
||||||
const hours = new Set(events.map(e => new Date(e.timestamp).getHours()))
|
|
||||||
if (hours.size === 1) {
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'suspicious-timing')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push('All activity in same hour of day')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 5: New account aggressive
|
|
||||||
if (accountAgeDays < 7 && totalRuns > 10) {
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'new-account-aggressive')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push(`Account ${accountAgeDays} days old with ${totalRuns} runs`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 6: Proxy flagged (heuristic: many ban hints)
|
|
||||||
const banHints = events.filter(e => e.type === 'ban_hint')
|
|
||||||
if (banHints.length >= 2) {
|
|
||||||
const p = this.patterns.find(pat => pat.name === 'proxy-flagged')
|
|
||||||
if (p) {
|
|
||||||
p.detected = true
|
|
||||||
p.evidence.push(`${banHints.length} ban hints detected`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate feature-based risk score (ML-style)
|
|
||||||
*/
|
|
||||||
private calculateFeatureScore(events: RiskEvent[], accountAgeDays: number, totalRuns: number): number {
|
|
||||||
let score = 0
|
|
||||||
|
|
||||||
// Feature 1: Event density (events per minute)
|
|
||||||
const eventDensity = events.length / 60
|
|
||||||
if (eventDensity > 0.5) score += 10
|
|
||||||
else if (eventDensity > 0.2) score += 5
|
|
||||||
|
|
||||||
// Feature 2: Account age risk
|
|
||||||
if (accountAgeDays < 3) score += 15
|
|
||||||
else if (accountAgeDays < 7) score += 10
|
|
||||||
else if (accountAgeDays < 14) score += 5
|
|
||||||
|
|
||||||
// Feature 3: Run frequency risk
|
|
||||||
const runsPerDay = totalRuns / Math.max(1, accountAgeDays)
|
|
||||||
if (runsPerDay > 3) score += 12
|
|
||||||
else if (runsPerDay > 2) score += 6
|
|
||||||
|
|
||||||
// Feature 4: Severity distribution
|
|
||||||
const highSeverityEvents = events.filter(e => e.severity >= 7)
|
|
||||||
if (highSeverityEvents.length > 3) score += 15
|
|
||||||
else if (highSeverityEvents.length > 1) score += 8
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Learn from historical data
|
|
||||||
*/
|
|
||||||
private getHistoricalAdjustment(email: string): number {
|
|
||||||
const accountHistory = this.history.filter(h => h.email === email)
|
|
||||||
if (accountHistory.length === 0) return 0
|
|
||||||
|
|
||||||
const bannedCount = accountHistory.filter(h => h.banned).length
|
|
||||||
const banRate = bannedCount / accountHistory.length
|
|
||||||
|
|
||||||
// If this account has high ban history, increase risk
|
|
||||||
if (banRate > 0.3) return 20
|
|
||||||
if (banRate > 0.1) return 10
|
|
||||||
|
|
||||||
// If clean history, slight bonus
|
|
||||||
if (accountHistory.length > 5 && banRate === 0) return -5
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate prediction confidence
|
|
||||||
*/
|
|
||||||
private calculateConfidence(eventCount: number, historyCount: number): number {
|
|
||||||
let confidence = 0.5
|
|
||||||
|
|
||||||
// More events = higher confidence
|
|
||||||
if (eventCount > 20) confidence += 0.2
|
|
||||||
else if (eventCount > 10) confidence += 0.1
|
|
||||||
|
|
||||||
// More historical data = higher confidence
|
|
||||||
if (historyCount > 50) confidence += 0.2
|
|
||||||
else if (historyCount > 20) confidence += 0.1
|
|
||||||
|
|
||||||
return Math.min(1.0, confidence)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate human-readable recommendation
|
|
||||||
*/
|
|
||||||
private generateRecommendation(score: number): string {
|
|
||||||
if (score < 20) {
|
|
||||||
return 'Safe to proceed. Risk is minimal.'
|
|
||||||
} else if (score < 40) {
|
|
||||||
return 'Low risk detected. Monitor for issues but safe to continue.'
|
|
||||||
} else if (score < 60) {
|
|
||||||
return 'Moderate risk. Consider increasing delays and reviewing patterns.'
|
|
||||||
} else if (score < 80) {
|
|
||||||
return 'High risk! Strongly recommend pausing automation for 24-48 hours.'
|
|
||||||
} else {
|
|
||||||
return 'CRITICAL RISK! Stop all automation immediately. Manual review required.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate actionable preventive steps
|
|
||||||
*/
|
|
||||||
private generatePreventiveActions(patterns: BanPattern[]): string[] {
|
|
||||||
const actions: string[] = []
|
|
||||||
|
|
||||||
if (patterns.some(p => p.name === 'rapid-captcha-sequence')) {
|
|
||||||
actions.push('Increase search delays to 3-5 minutes minimum')
|
|
||||||
actions.push('Enable longer cool-down periods between activities')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patterns.some(p => p.name === 'high-error-rate')) {
|
|
||||||
actions.push('Check proxy connectivity and health')
|
|
||||||
actions.push('Verify User-Agent and fingerprint configuration')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patterns.some(p => p.name === 'new-account-aggressive')) {
|
|
||||||
actions.push('Slow down activity on new accounts (max 1 run per day for first week)')
|
|
||||||
actions.push('Allow account to age naturally before heavy automation')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patterns.some(p => p.name === 'proxy-flagged')) {
|
|
||||||
actions.push('Rotate to different proxy immediately')
|
|
||||||
actions.push('Test proxy manually before resuming')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patterns.some(p => p.name === 'suspicious-timing')) {
|
|
||||||
actions.push('Randomize execution times across different hours')
|
|
||||||
actions.push('Enable humanization.allowedWindows with varied schedules')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.length === 0) {
|
|
||||||
actions.push('Continue monitoring but no immediate action needed')
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export historical data for analysis
|
|
||||||
*/
|
|
||||||
exportHistory(): HistoricalData[] {
|
|
||||||
return [...this.history]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import historical data (for persistence)
|
|
||||||
*/
|
|
||||||
importHistory(data: HistoricalData[]): void {
|
|
||||||
this.history = data.slice(-100) // Keep last 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,490 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import { Config } from '../interface/Config'
|
|
||||||
import { Account } from '../interface/Account'
|
|
||||||
|
|
||||||
export interface ValidationIssue {
|
|
||||||
severity: 'error' | 'warning' | 'info'
|
|
||||||
field: string
|
|
||||||
message: string
|
|
||||||
suggestion?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationResult {
|
|
||||||
valid: boolean
|
|
||||||
issues: ValidationIssue[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ConfigValidator performs intelligent validation of config.jsonc and accounts.json
|
|
||||||
* before execution to catch common mistakes, conflicts, and security issues.
|
|
||||||
*/
|
|
||||||
export class ConfigValidator {
|
|
||||||
/**
|
|
||||||
* Validate the main config file
|
|
||||||
*/
|
|
||||||
static validateConfig(config: Config): ValidationResult {
|
|
||||||
const issues: ValidationIssue[] = []
|
|
||||||
|
|
||||||
// Check baseURL
|
|
||||||
if (!config.baseURL || !config.baseURL.startsWith('https://')) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'baseURL',
|
|
||||||
message: 'baseURL must be a valid HTTPS URL',
|
|
||||||
suggestion: 'Use https://rewards.bing.com'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check sessionPath
|
|
||||||
if (!config.sessionPath || config.sessionPath.trim() === '') {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'sessionPath',
|
|
||||||
message: 'sessionPath cannot be empty'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check clusters
|
|
||||||
if (config.clusters < 1) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'clusters',
|
|
||||||
message: 'clusters must be at least 1'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (config.clusters > 10) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'clusters',
|
|
||||||
message: 'High cluster count may consume excessive resources',
|
|
||||||
suggestion: 'Consider using 2-4 clusters for optimal performance'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check globalTimeout
|
|
||||||
const timeout = this.parseTimeout(config.globalTimeout)
|
|
||||||
if (timeout < 10000) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'globalTimeout',
|
|
||||||
message: 'Very short timeout may cause frequent failures',
|
|
||||||
suggestion: 'Use at least 15s for stability'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (timeout > 120000) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'globalTimeout',
|
|
||||||
message: 'Very long timeout may slow down execution',
|
|
||||||
suggestion: 'Use 30-60s for optimal balance'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check search settings
|
|
||||||
if (config.searchSettings) {
|
|
||||||
const searchDelay = config.searchSettings.searchDelay
|
|
||||||
const minDelay = this.parseTimeout(searchDelay.min)
|
|
||||||
const maxDelay = this.parseTimeout(searchDelay.max)
|
|
||||||
|
|
||||||
if (minDelay >= maxDelay) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'searchSettings.searchDelay',
|
|
||||||
message: 'min delay must be less than max delay'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minDelay < 10000) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'searchSettings.searchDelay.min',
|
|
||||||
message: 'Very short search delays increase ban risk',
|
|
||||||
suggestion: 'Use at least 30s between searches'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.searchSettings.retryMobileSearchAmount > 5) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'searchSettings.retryMobileSearchAmount',
|
|
||||||
message: 'Too many retries may waste time',
|
|
||||||
suggestion: 'Use 2-3 retries maximum'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check humanization
|
|
||||||
if (config.humanization) {
|
|
||||||
if (config.humanization.enabled === false && config.humanization.stopOnBan === true) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'humanization',
|
|
||||||
message: 'stopOnBan is enabled but humanization is disabled',
|
|
||||||
suggestion: 'Enable humanization for better ban protection'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionDelay = config.humanization.actionDelay
|
|
||||||
if (actionDelay) {
|
|
||||||
const minAction = this.parseTimeout(actionDelay.min)
|
|
||||||
const maxAction = this.parseTimeout(actionDelay.max)
|
|
||||||
if (minAction >= maxAction) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'humanization.actionDelay',
|
|
||||||
message: 'min action delay must be less than max'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.humanization.allowedWindows && config.humanization.allowedWindows.length > 0) {
|
|
||||||
for (const window of config.humanization.allowedWindows) {
|
|
||||||
if (!/^\d{2}:\d{2}-\d{2}:\d{2}$/.test(window)) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'humanization.allowedWindows',
|
|
||||||
message: `Invalid time window format: ${window}`,
|
|
||||||
suggestion: 'Use format HH:mm-HH:mm (e.g., 09:00-17:00)'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check proxy config
|
|
||||||
if (config.proxy) {
|
|
||||||
if (config.proxy.proxyGoogleTrends === false && config.proxy.proxyBingTerms === false) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'info',
|
|
||||||
field: 'proxy',
|
|
||||||
message: 'All proxy options disabled - outbound requests will use direct connection'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check webhooks
|
|
||||||
if (config.webhook?.enabled && (!config.webhook.url || config.webhook.url.trim() === '')) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'webhook.url',
|
|
||||||
message: 'Webhook enabled but URL is empty'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.conclusionWebhook?.enabled && (!config.conclusionWebhook.url || config.conclusionWebhook.url.trim() === '')) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'conclusionWebhook.url',
|
|
||||||
message: 'Conclusion webhook enabled but URL is empty'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ntfy
|
|
||||||
if (config.ntfy?.enabled) {
|
|
||||||
if (!config.ntfy.url || config.ntfy.url.trim() === '') {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'ntfy.url',
|
|
||||||
message: 'NTFY enabled but URL is empty'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!config.ntfy.topic || config.ntfy.topic.trim() === '') {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'ntfy.topic',
|
|
||||||
message: 'NTFY enabled but topic is empty'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacySchedule = (config as unknown as { schedule?: unknown }).schedule
|
|
||||||
if (legacySchedule !== undefined) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'schedule',
|
|
||||||
message: 'Legacy schedule block detected.',
|
|
||||||
suggestion: 'Remove schedule.* entries and configure OS-level scheduling (docs/schedule.md).'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check workers
|
|
||||||
if (config.workers) {
|
|
||||||
const allDisabled = !config.workers.doDailySet &&
|
|
||||||
!config.workers.doMorePromotions &&
|
|
||||||
!config.workers.doPunchCards &&
|
|
||||||
!config.workers.doDesktopSearch &&
|
|
||||||
!config.workers.doMobileSearch &&
|
|
||||||
!config.workers.doDailyCheckIn &&
|
|
||||||
!config.workers.doReadToEarn
|
|
||||||
|
|
||||||
if (allDisabled) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'workers',
|
|
||||||
message: 'All workers are disabled - bot will not perform any tasks',
|
|
||||||
suggestion: 'Enable at least one worker type'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = !issues.some(i => i.severity === 'error')
|
|
||||||
return { valid, issues }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate accounts.json
|
|
||||||
*/
|
|
||||||
static validateAccounts(accounts: Account[]): ValidationResult {
|
|
||||||
const issues: ValidationIssue[] = []
|
|
||||||
|
|
||||||
if (accounts.length === 0) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: 'accounts',
|
|
||||||
message: 'No accounts found in accounts.json'
|
|
||||||
})
|
|
||||||
return { valid: false, issues }
|
|
||||||
}
|
|
||||||
|
|
||||||
const seenEmails = new Set<string>()
|
|
||||||
const seenProxies = new Map<string, string[]>() // proxy -> [emails]
|
|
||||||
|
|
||||||
for (let i = 0; i < accounts.length; i++) {
|
|
||||||
const acc = accounts[i]
|
|
||||||
const prefix = `accounts[${i}]`
|
|
||||||
|
|
||||||
if (!acc) continue
|
|
||||||
|
|
||||||
// Check email
|
|
||||||
if (!acc.email || acc.email.trim() === '') {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: `${prefix}.email`,
|
|
||||||
message: 'Account email is empty'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (seenEmails.has(acc.email)) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: `${prefix}.email`,
|
|
||||||
message: `Duplicate email: ${acc.email}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
seenEmails.add(acc.email)
|
|
||||||
|
|
||||||
if (!/@/.test(acc.email)) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: `${prefix}.email`,
|
|
||||||
message: 'Invalid email format'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check password
|
|
||||||
if (!acc.password || acc.password.trim() === '') {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: `${prefix}.password`,
|
|
||||||
message: 'Account password is empty'
|
|
||||||
})
|
|
||||||
} else if (acc.password.length < 8) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: `${prefix}.password`,
|
|
||||||
message: 'Very short password - verify it\'s correct'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check proxy
|
|
||||||
if (acc.proxy) {
|
|
||||||
const proxyUrl = acc.proxy.url
|
|
||||||
if (proxyUrl && proxyUrl.trim() !== '') {
|
|
||||||
if (!acc.proxy.port) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'error',
|
|
||||||
field: `${prefix}.proxy.port`,
|
|
||||||
message: 'Proxy URL specified but port is missing'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track proxy reuse
|
|
||||||
const proxyKey = `${proxyUrl}:${acc.proxy.port}`
|
|
||||||
if (!seenProxies.has(proxyKey)) {
|
|
||||||
seenProxies.set(proxyKey, [])
|
|
||||||
}
|
|
||||||
seenProxies.get(proxyKey)?.push(acc.email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check TOTP
|
|
||||||
if (acc.totp && acc.totp.trim() !== '') {
|
|
||||||
if (acc.totp.length < 16) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: `${prefix}.totp`,
|
|
||||||
message: 'TOTP secret seems too short - verify it\'s correct'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn about excessive proxy reuse
|
|
||||||
for (const [proxyKey, emails] of seenProxies) {
|
|
||||||
if (emails.length > 3) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'warning',
|
|
||||||
field: 'accounts.proxy',
|
|
||||||
message: `Proxy ${proxyKey} used by ${emails.length} accounts - may trigger rate limits`,
|
|
||||||
suggestion: 'Use different proxies per account for better safety'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = !issues.some(i => i.severity === 'error')
|
|
||||||
return { valid, issues }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate both config and accounts together (cross-checks)
|
|
||||||
*/
|
|
||||||
static validateAll(config: Config, accounts: Account[]): ValidationResult {
|
|
||||||
const configResult = this.validateConfig(config)
|
|
||||||
const accountsResult = this.validateAccounts(accounts)
|
|
||||||
|
|
||||||
const issues = [...configResult.issues, ...accountsResult.issues]
|
|
||||||
|
|
||||||
// Cross-validation: clusters vs accounts
|
|
||||||
if (accounts.length > 0 && config.clusters > accounts.length) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'info',
|
|
||||||
field: 'clusters',
|
|
||||||
message: `${config.clusters} clusters configured but only ${accounts.length} account(s)`,
|
|
||||||
suggestion: 'Reduce clusters to match account count for efficiency'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cross-validation: parallel mode with single account
|
|
||||||
if (config.parallel && accounts.length === 1) {
|
|
||||||
issues.push({
|
|
||||||
severity: 'info',
|
|
||||||
field: 'parallel',
|
|
||||||
message: 'Parallel mode enabled with single account has no effect',
|
|
||||||
suggestion: 'Disable parallel mode or add more accounts'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = !issues.some(i => i.severity === 'error')
|
|
||||||
return { valid, issues }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load and validate from file paths
|
|
||||||
*/
|
|
||||||
static validateFromFiles(configPath: string, accountsPath: string): ValidationResult {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
issues: [{
|
|
||||||
severity: 'error',
|
|
||||||
field: 'config',
|
|
||||||
message: `Config file not found: ${configPath}`
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(accountsPath)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
issues: [{
|
|
||||||
severity: 'error',
|
|
||||||
field: 'accounts',
|
|
||||||
message: `Accounts file not found: ${accountsPath}`
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const configRaw = fs.readFileSync(configPath, 'utf-8')
|
|
||||||
const accountsRaw = fs.readFileSync(accountsPath, 'utf-8')
|
|
||||||
|
|
||||||
// Remove JSONC comments (basic approach)
|
|
||||||
const configJson = configRaw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
|
|
||||||
const config: Config = JSON.parse(configJson)
|
|
||||||
const accounts: Account[] = JSON.parse(accountsRaw)
|
|
||||||
|
|
||||||
return this.validateAll(config, accounts)
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
issues: [{
|
|
||||||
severity: 'error',
|
|
||||||
field: 'parse',
|
|
||||||
message: `Failed to parse files: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print validation results to console with color
|
|
||||||
* Note: This method intentionally uses console.log for CLI output formatting
|
|
||||||
*/
|
|
||||||
static printResults(result: ValidationResult): void {
|
|
||||||
if (result.valid) {
|
|
||||||
console.log('✅ Configuration validation passed\n')
|
|
||||||
} else {
|
|
||||||
console.log('❌ Configuration validation failed\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.issues.length === 0) {
|
|
||||||
console.log('No issues found.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = result.issues.filter(i => i.severity === 'error')
|
|
||||||
const warnings = result.issues.filter(i => i.severity === 'warning')
|
|
||||||
const infos = result.issues.filter(i => i.severity === 'info')
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.log(`\n🚫 ERRORS (${errors.length}):`)
|
|
||||||
for (const issue of errors) {
|
|
||||||
console.log(` ${issue.field}: ${issue.message}`)
|
|
||||||
if (issue.suggestion) {
|
|
||||||
console.log(` → ${issue.suggestion}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (warnings.length > 0) {
|
|
||||||
console.log(`\n⚠️ WARNINGS (${warnings.length}):`)
|
|
||||||
for (const issue of warnings) {
|
|
||||||
console.log(` ${issue.field}: ${issue.message}`)
|
|
||||||
if (issue.suggestion) {
|
|
||||||
console.log(` → ${issue.suggestion}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (infos.length > 0) {
|
|
||||||
console.log(`\nℹ️ INFO (${infos.length}):`)
|
|
||||||
for (const issue of infos) {
|
|
||||||
console.log(` ${issue.field}: ${issue.message}`)
|
|
||||||
if (issue.suggestion) {
|
|
||||||
console.log(` → ${issue.suggestion}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
|
|
||||||
private static parseTimeout(value: number | string): number {
|
|
||||||
if (typeof value === 'number') return value
|
|
||||||
const str = String(value).toLowerCase()
|
|
||||||
if (str.endsWith('ms')) return parseInt(str, 10)
|
|
||||||
if (str.endsWith('s')) return parseInt(str, 10) * 1000
|
|
||||||
if (str.endsWith('min')) return parseInt(str, 10) * 60000
|
|
||||||
return parseInt(str, 10) || 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { AdaptiveThrottler } from './AdaptiveThrottler'
|
|
||||||
|
|
||||||
export interface RiskEvent {
|
|
||||||
type: 'captcha' | 'error' | 'timeout' | 'ban_hint' | 'success'
|
|
||||||
timestamp: number
|
|
||||||
severity: number // 0-10, higher = worse
|
|
||||||
context?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RiskMetrics {
|
|
||||||
score: number // 0-100, higher = riskier
|
|
||||||
level: 'safe' | 'elevated' | 'high' | 'critical'
|
|
||||||
recommendation: string
|
|
||||||
delayMultiplier: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RiskManager monitors account activity patterns and detects early ban signals.
|
|
||||||
* Integrates with AdaptiveThrottler to dynamically adjust delays based on risk.
|
|
||||||
*/
|
|
||||||
export class RiskManager {
|
|
||||||
private events: RiskEvent[] = []
|
|
||||||
private readonly maxEvents = 100
|
|
||||||
private readonly timeWindowMs = 3600000 // 1 hour
|
|
||||||
private throttler: AdaptiveThrottler
|
|
||||||
private cooldownUntil: number = 0
|
|
||||||
|
|
||||||
constructor(throttler?: AdaptiveThrottler) {
|
|
||||||
this.throttler = throttler || new AdaptiveThrottler()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a risk event (captcha, error, success, etc.)
|
|
||||||
*/
|
|
||||||
recordEvent(type: RiskEvent['type'], severity: number, context?: string): void {
|
|
||||||
const event: RiskEvent = {
|
|
||||||
type,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
severity: Math.max(0, Math.min(10, severity)),
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
this.events.push(event)
|
|
||||||
if (this.events.length > this.maxEvents) {
|
|
||||||
this.events.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feed success/error into adaptive throttler
|
|
||||||
if (type === 'success') {
|
|
||||||
this.throttler.record(true)
|
|
||||||
} else if (['error', 'captcha', 'timeout', 'ban_hint'].includes(type)) {
|
|
||||||
this.throttler.record(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto cool-down on critical events
|
|
||||||
if (severity >= 8) {
|
|
||||||
const coolMs = Math.min(300000, severity * 30000) // max 5min
|
|
||||||
this.cooldownUntil = Date.now() + coolMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate current risk metrics based on recent events
|
|
||||||
*/
|
|
||||||
assessRisk(): RiskMetrics {
|
|
||||||
const now = Date.now()
|
|
||||||
const recentEvents = this.events.filter(e => now - e.timestamp < this.timeWindowMs)
|
|
||||||
|
|
||||||
if (recentEvents.length === 0) {
|
|
||||||
return {
|
|
||||||
score: 0,
|
|
||||||
level: 'safe',
|
|
||||||
recommendation: 'Normal operation',
|
|
||||||
delayMultiplier: 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate base risk score (weighted by recency and severity)
|
|
||||||
let weightedSum = 0
|
|
||||||
let totalWeight = 0
|
|
||||||
|
|
||||||
for (const event of recentEvents) {
|
|
||||||
const age = now - event.timestamp
|
|
||||||
const recencyFactor = 1 - (age / this.timeWindowMs) // newer = higher weight
|
|
||||||
const weight = recencyFactor * (event.severity / 10)
|
|
||||||
|
|
||||||
weightedSum += weight * event.severity
|
|
||||||
totalWeight += weight
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseScore = totalWeight > 0 ? (weightedSum / totalWeight) * 10 : 0
|
|
||||||
|
|
||||||
// Penalty for rapid event frequency
|
|
||||||
const eventRate = recentEvents.length / (this.timeWindowMs / 60000) // events per minute
|
|
||||||
const frequencyPenalty = Math.min(30, eventRate * 5)
|
|
||||||
|
|
||||||
// Bonus penalty for specific patterns
|
|
||||||
const captchaCount = recentEvents.filter(e => e.type === 'captcha').length
|
|
||||||
const banHintCount = recentEvents.filter(e => e.type === 'ban_hint').length
|
|
||||||
const patternPenalty = (captchaCount * 15) + (banHintCount * 25)
|
|
||||||
|
|
||||||
const finalScore = Math.min(100, baseScore + frequencyPenalty + patternPenalty)
|
|
||||||
|
|
||||||
// Determine risk level
|
|
||||||
let level: RiskMetrics['level']
|
|
||||||
let recommendation: string
|
|
||||||
let delayMultiplier: number
|
|
||||||
|
|
||||||
if (finalScore < 20) {
|
|
||||||
level = 'safe'
|
|
||||||
recommendation = 'Normal operation'
|
|
||||||
delayMultiplier = 1.0
|
|
||||||
} else if (finalScore < 40) {
|
|
||||||
level = 'elevated'
|
|
||||||
recommendation = 'Minor issues detected. Increasing delays slightly.'
|
|
||||||
delayMultiplier = 1.5
|
|
||||||
} else if (finalScore < 70) {
|
|
||||||
level = 'high'
|
|
||||||
recommendation = 'Significant risk detected. Applying heavy throttling.'
|
|
||||||
delayMultiplier = 2.5
|
|
||||||
} else {
|
|
||||||
level = 'critical'
|
|
||||||
recommendation = 'CRITICAL: High ban risk. Consider stopping or manual review.'
|
|
||||||
delayMultiplier = 4.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply adaptive throttler multiplier on top
|
|
||||||
const adaptiveMultiplier = this.throttler.getDelayMultiplier()
|
|
||||||
delayMultiplier *= adaptiveMultiplier
|
|
||||||
|
|
||||||
return {
|
|
||||||
score: Math.round(finalScore),
|
|
||||||
level,
|
|
||||||
recommendation,
|
|
||||||
delayMultiplier: Number(delayMultiplier.toFixed(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if currently in forced cool-down period
|
|
||||||
*/
|
|
||||||
isInCooldown(): boolean {
|
|
||||||
return Date.now() < this.cooldownUntil
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get remaining cool-down time in milliseconds
|
|
||||||
*/
|
|
||||||
getCooldownRemaining(): number {
|
|
||||||
const remaining = this.cooldownUntil - Date.now()
|
|
||||||
return Math.max(0, remaining)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the adaptive throttler instance for advanced usage
|
|
||||||
*/
|
|
||||||
getThrottler(): AdaptiveThrottler {
|
|
||||||
return this.throttler
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all events and reset state (use between accounts)
|
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.events = []
|
|
||||||
this.cooldownUntil = 0
|
|
||||||
// Keep throttler state across resets for learning
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export events for analytics/logging
|
|
||||||
*/
|
|
||||||
getRecentEvents(limitMinutes: number = 60): RiskEvent[] {
|
|
||||||
const cutoff = Date.now() - (limitMinutes * 60000)
|
|
||||||
return this.events.filter(e => e.timestamp >= cutoff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import type { Page } from 'playwright'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security incident detected during login/authentication
|
|
||||||
*/
|
|
||||||
export interface SecurityIncident {
|
|
||||||
kind: string
|
|
||||||
account: string
|
|
||||||
details?: string[]
|
|
||||||
next?: string[]
|
|
||||||
docsUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SecurityDetector: Centralized detection of login security blocks and anomalies
|
|
||||||
* Extracted from Login.ts for testability and separation of concerns
|
|
||||||
*/
|
|
||||||
export class SecurityDetector {
|
|
||||||
// Sign-in block patterns (Microsoft security messages)
|
|
||||||
private static readonly SIGN_IN_BLOCK_PATTERNS: { re: RegExp; label: string }[] = [
|
|
||||||
{ re: /we can[''`]?t sign you in/i, label: 'cant-sign-in' },
|
|
||||||
{ re: /incorrect account or password too many times/i, label: 'too-many-incorrect' },
|
|
||||||
{ re: /used an incorrect account or password too many times/i, label: 'too-many-incorrect-variant' },
|
|
||||||
{ re: /sign-in has been blocked/i, label: 'sign-in-blocked-phrase' },
|
|
||||||
{ re: /your account has been locked/i, label: 'account-locked' },
|
|
||||||
{ re: /your account or password is incorrect too many times/i, label: 'incorrect-too-many-times' }
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if page contains sign-in blocked message
|
|
||||||
* Returns matched pattern label or null
|
|
||||||
*/
|
|
||||||
static async detectSignInBlocked(page: Page): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
let text = ''
|
|
||||||
const selectors = ['[data-testid="title"]', 'h1', 'div[role="heading"]', 'div.text-title']
|
|
||||||
|
|
||||||
for (const sel of selectors) {
|
|
||||||
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(() => null)
|
|
||||||
if (el) {
|
|
||||||
const t = (await el.textContent() || '').trim()
|
|
||||||
if (t && t.length < 300) text += ' ' + t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lower = text.toLowerCase()
|
|
||||||
for (const p of SecurityDetector.SIGN_IN_BLOCK_PATTERNS) {
|
|
||||||
if (p.re.test(lower)) {
|
|
||||||
return p.label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse masked email from Microsoft recovery prompts
|
|
||||||
* Returns { prefix: string, domain: string } or null
|
|
||||||
* Examples: "k*****@domain.com" → { prefix: "k", domain: "domain.com" }
|
|
||||||
* "ko****@domain.com" → { prefix: "ko", domain: "domain.com" }
|
|
||||||
*/
|
|
||||||
static parseMaskedEmail(masked: string): { prefix: string; domain: string } | null {
|
|
||||||
// Pattern: 1-2 visible chars, then masked chars, then @domain
|
|
||||||
const regex = /([a-zA-Z0-9]{1,2})[a-zA-Z0-9*•._-]*@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/
|
|
||||||
const match = regex.exec(masked)
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
// Fallback: try looser pattern
|
|
||||||
const loose = /([a-zA-Z0-9])[*•][a-zA-Z0-9*•._-]*@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/.exec(masked)
|
|
||||||
if (!loose) return null
|
|
||||||
return {
|
|
||||||
prefix: (loose[1] || '').toLowerCase(),
|
|
||||||
domain: (loose[2] || '').toLowerCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix: (match[1] || '').toLowerCase(),
|
|
||||||
domain: (match[2] || '').toLowerCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if masked email matches expected recovery email
|
|
||||||
* Lenient matching: only compare visible prefix (1-2 chars) + domain
|
|
||||||
*/
|
|
||||||
static matchesMaskedEmail(
|
|
||||||
observed: { prefix: string; domain: string },
|
|
||||||
expected: { prefix: string; domain: string }
|
|
||||||
): boolean {
|
|
||||||
if (observed.domain !== expected.domain) return false
|
|
||||||
|
|
||||||
// If only 1 char visible, match first char only
|
|
||||||
if (observed.prefix.length === 1) {
|
|
||||||
return expected.prefix.startsWith(observed.prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If 2 chars visible, both must match
|
|
||||||
return expected.prefix === observed.prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract all masked emails from page (candidates for recovery email check)
|
|
||||||
* Returns array of masked email strings found in DOM
|
|
||||||
*/
|
|
||||||
static async extractRecoveryCandidates(page: Page): Promise<string[]> {
|
|
||||||
const candidates: string[] = []
|
|
||||||
|
|
||||||
// Priority 1: Direct selectors (Microsoft variants + French)
|
|
||||||
const directSelectors = [
|
|
||||||
'[data-testid="recoveryEmailHint"]',
|
|
||||||
'#recoveryEmail',
|
|
||||||
'[id*="ProofEmail"]',
|
|
||||||
'[id*="EmailProof"]',
|
|
||||||
'[data-testid*="Email"]',
|
|
||||||
'span:has(span.fui-Text)'
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const sel of directSelectors) {
|
|
||||||
const el = await page.waitForSelector(sel, { timeout: 1000 }).catch(() => null)
|
|
||||||
if (el) {
|
|
||||||
const t = (await el.textContent() || '').trim()
|
|
||||||
if (t) candidates.push(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: List items
|
|
||||||
const listItems = page.locator('[role="listitem"], li')
|
|
||||||
const count = await listItems.count().catch(() => 0)
|
|
||||||
for (let i = 0; i < Math.min(count, 12); i++) {
|
|
||||||
const t = (await listItems.nth(i).textContent().catch(() => ''))?.trim() || ''
|
|
||||||
if (t && /@/.test(t)) candidates.push(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 3: XPath generic masked patterns
|
|
||||||
const xpath = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]')
|
|
||||||
const xpCount = await xpath.count().catch(() => 0)
|
|
||||||
for (let i = 0; i < Math.min(xpCount, 12); i++) {
|
|
||||||
const t = (await xpath.nth(i).textContent().catch(() => ''))?.trim() || ''
|
|
||||||
if (t && t.length < 300) candidates.push(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 4: Full HTML scan fallback
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
try {
|
|
||||||
const html = await page.content()
|
|
||||||
const generic = /[A-Za-z0-9]{1,4}[*•]{2,}[A-Za-z0-9*•._-]*@[A-Za-z0-9.-]+/g
|
|
||||||
const frPhrase = /Nous\s+enverrons\s+un\s+code\s+à\s+([^<@]*[A-Za-z0-9]{1,4}[*•]{2,}[A-Za-z0-9*•._-]*@[A-Za-z0-9.-]+)[^.]{0,120}?Pour\s+vérifier/gi
|
|
||||||
|
|
||||||
const found = new Set<string>()
|
|
||||||
let m: RegExpExecArray | null
|
|
||||||
while ((m = generic.exec(html)) !== null) found.add(m[0])
|
|
||||||
while ((m = frPhrase.exec(html)) !== null) {
|
|
||||||
const raw = m[1]?.replace(/<[^>]+>/g, '').trim()
|
|
||||||
if (raw) found.add(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.push(...Array.from(found))
|
|
||||||
} catch {
|
|
||||||
/* ignore HTML scan errors */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplicate
|
|
||||||
const seen = new Set<string>()
|
|
||||||
return candidates
|
|
||||||
.map(s => s.replace(/\s+/g, ' ').trim())
|
|
||||||
.filter(t => t && !seen.has(t) && seen.add(t))
|
|
||||||
.filter(t => /@/.test(t) && /[*•]/.test(t)) // Must be masked email
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if page contains account locked indicator
|
|
||||||
*/
|
|
||||||
static async detectAccountLocked(page: Page): Promise<boolean> {
|
|
||||||
return await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse email into { prefix: first 2 chars, domain }
|
|
||||||
*/
|
|
||||||
static parseEmailReference(email: string): { prefix: string; domain: string } | null {
|
|
||||||
if (!email || !/@/.test(email)) return null
|
|
||||||
|
|
||||||
const [local, domain] = email.split('@')
|
|
||||||
if (!local || !domain) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix: local.slice(0, 2).toLowerCase(),
|
|
||||||
domain: domain.toLowerCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get documentation URL for security incident
|
|
||||||
*/
|
|
||||||
static getDocsUrl(anchor?: string): string {
|
|
||||||
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/Obsidian-wtf/Microsoft-Rewards-Bot/blob/main/docs/security.md'
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
'recovery-email-mismatch': '#recovery-email-mismatch',
|
|
||||||
'we-cant-sign-you-in': '#we-cant-sign-you-in-blocked'
|
|
||||||
}
|
|
||||||
return anchor && map[anchor] ? `${base}${map[anchor]}` : base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import ms from 'ms'
|
import ms from 'ms'
|
||||||
|
|
||||||
|
export function getErrorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
export class Util {
|
export class Util {
|
||||||
|
|
||||||
async wait(ms: number): Promise<void> {
|
wait(ms: number): Promise<void> {
|
||||||
const MAX_WAIT_MS = 3600000 // 1 hour max to prevent infinite waits
|
const MAX_WAIT_MS = 3600000 // 1 hour max to prevent infinite waits
|
||||||
const MIN_WAIT_MS = 0
|
const MIN_WAIT_MS = 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user