mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
733 lines
24 KiB
TypeScript
733 lines
24 KiB
TypeScript
import chalk from 'chalk'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { Account } from '../../interface/Account'
|
|
import { Config } from '../../interface/Config'
|
|
import { log } from '../notifications/Logger'
|
|
|
|
interface ValidationError {
|
|
severity: 'error' | 'warning'
|
|
category: string
|
|
message: string
|
|
fix?: string
|
|
docsLink?: string
|
|
blocking?: boolean // If true, prevents bot startup
|
|
}
|
|
|
|
export class StartupValidator {
|
|
private errors: ValidationError[] = []
|
|
private warnings: ValidationError[] = []
|
|
|
|
/**
|
|
* Run all validation checks before starting the bot.
|
|
* Throws ValidationError if critical (blocking) errors are found.
|
|
* Displays errors and warnings to help users fix configuration issues.
|
|
*/
|
|
async validate(config: Config, accounts: Account[]): Promise<boolean> {
|
|
log('main', 'STARTUP', 'Running configuration validation...')
|
|
|
|
// Run all validation checks
|
|
this.validateAccounts(accounts)
|
|
this.validateConfig(config)
|
|
this.validateEnvironment()
|
|
this.validateFileSystem(config)
|
|
this.validateBrowserSettings(config)
|
|
this.validateNetworkSettings(config)
|
|
this.validateWorkerSettings(config)
|
|
this.validateExecutionSettings(config)
|
|
this.validateSearchSettings(config)
|
|
this.validateHumanizationSettings(config)
|
|
this.validateSecuritySettings(config)
|
|
|
|
// Display results (await to respect the delay)
|
|
await this.displayResults()
|
|
|
|
// Check for blocking errors
|
|
const blockingErrors = this.errors.filter(e => e.blocking === true)
|
|
if (blockingErrors.length > 0) {
|
|
const errorMsg = `Validation failed with ${blockingErrors.length} critical error(s). Fix configuration before proceeding.`
|
|
log('main', 'VALIDATION', errorMsg, 'error')
|
|
throw new Error(errorMsg)
|
|
}
|
|
|
|
// Non-blocking errors and warnings allow execution to continue
|
|
return true
|
|
}
|
|
|
|
private validateAccounts(accounts: Account[]): void {
|
|
if (!accounts || accounts.length === 0) {
|
|
this.addError(
|
|
'accounts',
|
|
'No accounts found in accounts.json',
|
|
'Add at least one account to src/accounts.json or src/accounts.jsonc',
|
|
'docs/accounts.md',
|
|
true // blocking: no accounts = nothing to run
|
|
)
|
|
return
|
|
}
|
|
|
|
accounts.forEach((account, index) => {
|
|
const prefix = `Account ${index + 1} (${account.email || 'unknown'})`
|
|
|
|
// Required: email
|
|
if (!account.email || typeof account.email !== 'string') {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: Missing or invalid email address`,
|
|
'Add a valid email address in the "email" field',
|
|
undefined,
|
|
true // blocking: email is required
|
|
)
|
|
} else if (!/@/.test(account.email)) {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: Email format is invalid`,
|
|
'Email must contain @ symbol (e.g., user@example.com)',
|
|
undefined,
|
|
true // blocking: invalid email = cannot login
|
|
)
|
|
}
|
|
|
|
// Required: password
|
|
if (!account.password || typeof account.password !== 'string') {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: Missing or invalid password`,
|
|
'Add your Microsoft account password in the "password" field',
|
|
undefined,
|
|
true // blocking: password is required
|
|
)
|
|
} else if (account.password.length < 4) {
|
|
this.addWarning(
|
|
'accounts',
|
|
`${prefix}: Password seems too short (${account.password.length} characters)`,
|
|
'Verify this is your correct Microsoft account password'
|
|
)
|
|
}
|
|
|
|
// Simplified: only validate recovery email if provided
|
|
if (account.recoveryEmail && typeof account.recoveryEmail === 'string' && account.recoveryEmail.trim() !== '') {
|
|
if (!/@/.test(account.recoveryEmail)) {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: Recovery email format is invalid`,
|
|
'Recovery email must be a valid email address (e.g., backup@gmail.com)'
|
|
)
|
|
}
|
|
} else {
|
|
this.addWarning(
|
|
'accounts',
|
|
`${prefix}: No recovery email configured`,
|
|
'Recovery email is optional but recommended for security challenge verification',
|
|
'docs/accounts.md'
|
|
)
|
|
}
|
|
|
|
// Optional but recommended: TOTP
|
|
if (!account.totp || account.totp.trim() === '') {
|
|
this.addWarning(
|
|
'accounts',
|
|
`${prefix}: No TOTP (2FA) secret configured`,
|
|
'Highly recommended: Set up 2FA and add your TOTP secret for automated login',
|
|
'docs/accounts.md'
|
|
)
|
|
} else {
|
|
const cleaned = account.totp.replace(/\s+/g, '')
|
|
if (cleaned.length < 16) {
|
|
this.addWarning(
|
|
'accounts',
|
|
`${prefix}: TOTP secret seems too short (${cleaned.length} chars)`,
|
|
'Verify you copied the complete Base32 secret from Microsoft Authenticator setup'
|
|
)
|
|
}
|
|
// Check if it's Base32 (A-Z, 2-7)
|
|
if (!/^[A-Z2-7\s]+$/i.test(account.totp)) {
|
|
this.addWarning(
|
|
'accounts',
|
|
`${prefix}: TOTP secret contains invalid characters`,
|
|
'TOTP secrets should only contain letters A-Z and numbers 2-7 (Base32 format)'
|
|
)
|
|
}
|
|
}
|
|
|
|
// Proxy validation
|
|
if (account.proxy) {
|
|
const hasProxyUrl = account.proxy.url && account.proxy.url.trim() !== ''
|
|
const proxyEnabled = account.proxy.proxyAxios === true
|
|
|
|
if (proxyEnabled && !hasProxyUrl) {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: proxyAxios is true but proxy URL is empty`,
|
|
'Set proxyAxios to false if not using a proxy, or provide valid proxy URL/port',
|
|
undefined,
|
|
true // blocking
|
|
)
|
|
}
|
|
|
|
if (hasProxyUrl) {
|
|
if (!account.proxy.port || account.proxy.port <= 0) {
|
|
this.addError(
|
|
'accounts',
|
|
`${prefix}: Proxy URL provided but port is missing or invalid`,
|
|
'Add a valid proxy port number (e.g., 8080, 3128)'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private validateConfig(config: Config): void {
|
|
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
|
|
if (maybeSchedule !== undefined) {
|
|
this.addWarning(
|
|
'config',
|
|
'Legacy schedule settings detected in config.jsonc.',
|
|
'Remove schedule.* entries and use your operating system scheduler.',
|
|
'docs/schedule.md'
|
|
)
|
|
}
|
|
|
|
// Headless mode in Docker
|
|
if (process.env.FORCE_HEADLESS === '1' && config.browser?.headless === false) {
|
|
this.addWarning(
|
|
'config',
|
|
'FORCE_HEADLESS=1 but config.browser.headless is false',
|
|
'Docker environment forces headless mode. Your config setting will be overridden.'
|
|
)
|
|
}
|
|
|
|
// Parallel mode warning
|
|
if (config.parallel === true) {
|
|
this.addWarning(
|
|
'config',
|
|
'Parallel mode enabled (desktop + mobile run simultaneously)',
|
|
'This uses more resources. Disable if you experience crashes or timeouts.',
|
|
'docs/config.md'
|
|
)
|
|
}
|
|
|
|
// Clusters validation
|
|
if (config.clusters > 1) {
|
|
this.addWarning(
|
|
'config',
|
|
`Clusters set to ${config.clusters} - accounts will run in parallel`,
|
|
'Ensure your system has enough resources (RAM, CPU) for concurrent execution'
|
|
)
|
|
}
|
|
|
|
// Global timeout validation
|
|
const timeout = typeof config.globalTimeout === 'string'
|
|
? config.globalTimeout
|
|
: `${config.globalTimeout}ms`
|
|
|
|
if (timeout === '0' || timeout === '0ms' || timeout === '0s') {
|
|
this.addError(
|
|
'config',
|
|
'Global timeout is set to 0',
|
|
'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs',
|
|
undefined,
|
|
true // blocking: 0 timeout = infinite hangs guaranteed
|
|
)
|
|
}
|
|
|
|
// Job state validation
|
|
if (config.jobState?.enabled === false) {
|
|
this.addWarning(
|
|
'config',
|
|
'Job state tracking is disabled',
|
|
'The bot will not save progress. If interrupted, all tasks will restart from scratch.',
|
|
'docs/jobstate.md'
|
|
)
|
|
}
|
|
|
|
// Risk management validation
|
|
if (config.riskManagement?.enabled === true) {
|
|
// If risk management is enabled, notify the user to ensure policies are configured.
|
|
// This avoids an empty-block lint/compile error and provides actionable guidance.
|
|
this.addWarning(
|
|
'riskManagement',
|
|
'Risk management is enabled but no specific policies were validated here',
|
|
'Review and configure riskManagement settings (throttles, maxRestarts, detection thresholds)',
|
|
'docs/config.md'
|
|
)
|
|
}
|
|
|
|
// Search delays validation
|
|
const minDelay = typeof config.searchSettings.searchDelay.min === 'string'
|
|
? config.searchSettings.searchDelay.min
|
|
: `${config.searchSettings.searchDelay.min}ms`
|
|
|
|
if (minDelay === '0' || minDelay === '0ms' || minDelay === '0s') {
|
|
this.addWarning(
|
|
'config',
|
|
'Search delay minimum is 0 - this may look suspicious',
|
|
'Consider setting a minimum delay (e.g., "1s", "2s") for more natural behavior'
|
|
)
|
|
}
|
|
}
|
|
|
|
private validateEnvironment(): void {
|
|
// Node.js version check
|
|
const nodeVersion = process.version
|
|
const major = parseInt(nodeVersion.split('.')[0]?.replace('v', '') || '0', 10)
|
|
|
|
if (major < 18) {
|
|
this.addError(
|
|
'environment',
|
|
`Node.js version ${nodeVersion} is too old`,
|
|
'Install Node.js 18 or newer. Visit https://nodejs.org/',
|
|
'docs/getting-started.md'
|
|
)
|
|
} else if (major < 20) {
|
|
this.addWarning(
|
|
'environment',
|
|
`Node.js version ${nodeVersion} is outdated`,
|
|
'Consider upgrading to Node.js 20+ for better performance and security'
|
|
)
|
|
}
|
|
|
|
// Docker-specific checks
|
|
if (process.env.FORCE_HEADLESS === '1') {
|
|
this.addWarning(
|
|
'environment',
|
|
'Running in Docker/containerized environment',
|
|
'Make sure volumes are correctly mounted for sessions persistence'
|
|
)
|
|
}
|
|
|
|
// Time sync info for TOTP users (informational, not a problem)
|
|
if (process.platform === 'linux') {
|
|
// This is just informational - not displayed as warning
|
|
log('main', 'VALIDATION', '💡 Linux detected: Ensure system time is synchronized for TOTP')
|
|
log('main', 'VALIDATION', ' Suggestion: Run: sudo timedatectl set-ntp true (required for TOTP to work correctly)')
|
|
}
|
|
}
|
|
|
|
private validateFileSystem(config: Config): void {
|
|
// Check if sessions directory exists or can be created
|
|
const sessionPath = path.isAbsolute(config.sessionPath)
|
|
? config.sessionPath
|
|
: path.join(process.cwd(), config.sessionPath)
|
|
|
|
if (!fs.existsSync(sessionPath)) {
|
|
try {
|
|
fs.mkdirSync(sessionPath, { recursive: true })
|
|
this.addWarning(
|
|
'filesystem',
|
|
`Created missing sessions directory: ${sessionPath}`,
|
|
'Session data will be stored here'
|
|
)
|
|
} catch (error) {
|
|
this.addError(
|
|
'filesystem',
|
|
`Cannot create sessions directory: ${sessionPath}`,
|
|
`Check file permissions. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
)
|
|
}
|
|
}
|
|
|
|
// Check job-state directory if enabled
|
|
if (config.jobState?.enabled !== false) {
|
|
const jobStateDir = config.jobState?.dir
|
|
? config.jobState.dir
|
|
: path.join(sessionPath, 'job-state')
|
|
|
|
if (!fs.existsSync(jobStateDir)) {
|
|
try {
|
|
fs.mkdirSync(jobStateDir, { recursive: true })
|
|
} catch (error) {
|
|
this.addWarning(
|
|
'filesystem',
|
|
`Cannot create job-state directory: ${jobStateDir}`,
|
|
'Job state tracking may fail. Check file permissions.'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private validateBrowserSettings(config: Config): void {
|
|
// Headless validation - only warn in Docker/containerized environments
|
|
if (!config.browser?.headless && process.env.FORCE_HEADLESS === '1') {
|
|
this.addWarning(
|
|
'browser',
|
|
'FORCE_HEADLESS=1 but config.browser.headless is false',
|
|
'Docker environment forces headless mode. Your config setting will be overridden.',
|
|
'docs/docker.md'
|
|
)
|
|
}
|
|
|
|
// Fingerprinting validation
|
|
if (config.saveFingerprint?.desktop === false && config.saveFingerprint?.mobile === false) {
|
|
this.addWarning(
|
|
'browser',
|
|
'Fingerprint saving is completely disabled',
|
|
'Each run will generate new fingerprints, which may look suspicious'
|
|
)
|
|
}
|
|
}
|
|
|
|
private validateNetworkSettings(config: Config): void {
|
|
// Webhook validation
|
|
if (config.webhook?.enabled === true) {
|
|
if (!config.webhook.url || config.webhook.url.trim() === '') {
|
|
this.addError(
|
|
'network',
|
|
'Webhook enabled but URL is missing',
|
|
'Add webhook URL or set webhook.enabled=false',
|
|
'docs/config.md',
|
|
true // blocking: enabled but no URL = will crash
|
|
)
|
|
} else if (!config.webhook.url.startsWith('http')) {
|
|
this.addError(
|
|
'network',
|
|
`Invalid webhook URL: ${config.webhook.url}`,
|
|
'Webhook URL must start with http:// or https://',
|
|
undefined,
|
|
true // blocking: invalid URL = will crash
|
|
)
|
|
}
|
|
}
|
|
|
|
// Conclusion webhook validation
|
|
if (config.conclusionWebhook?.enabled === true) {
|
|
if (!config.conclusionWebhook.url || config.conclusionWebhook.url.trim() === '') {
|
|
this.addError(
|
|
'network',
|
|
'Conclusion webhook enabled but URL is missing',
|
|
'Add conclusion webhook URL or disable it',
|
|
undefined,
|
|
true // blocking: enabled but no URL = will crash
|
|
)
|
|
}
|
|
}
|
|
|
|
// NTFY validation
|
|
if (config.ntfy?.enabled === true) {
|
|
if (!config.ntfy.url || config.ntfy.url.trim() === '') {
|
|
this.addError(
|
|
'network',
|
|
'NTFY enabled but URL is missing',
|
|
'Add NTFY server URL or set ntfy.enabled=false',
|
|
'docs/ntfy.md',
|
|
true // blocking: enabled but no URL = will crash
|
|
)
|
|
}
|
|
if (!config.ntfy.topic || config.ntfy.topic.trim() === '') {
|
|
this.addError(
|
|
'network',
|
|
'NTFY enabled but topic is missing',
|
|
'Add NTFY topic name',
|
|
'docs/ntfy.md',
|
|
true // blocking: enabled but no topic = will crash
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private validateWorkerSettings(config: Config): void {
|
|
const workers = config.workers
|
|
|
|
// Check if at least one worker is enabled
|
|
const anyEnabled = workers.doDailySet || workers.doMorePromotions || workers.doPunchCards ||
|
|
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
|
|
workers.doReadToEarn
|
|
|
|
if (!anyEnabled) {
|
|
this.addWarning(
|
|
'workers',
|
|
'All workers are disabled - bot will do nothing',
|
|
'Enable at least one worker task (doDailySet, doDesktopSearch, etc.)',
|
|
'docs/config.md'
|
|
)
|
|
}
|
|
|
|
// Mobile + desktop search check
|
|
if (!workers.doDesktopSearch && !workers.doMobileSearch) {
|
|
this.addWarning(
|
|
'workers',
|
|
'Both desktop and mobile searches are disabled',
|
|
'Enable at least one search type to earn search points'
|
|
)
|
|
}
|
|
|
|
// Bundle validation
|
|
if (workers.bundleDailySetWithSearch === true && !workers.doDesktopSearch) {
|
|
this.addWarning(
|
|
'workers',
|
|
'bundleDailySetWithSearch is enabled but doDesktopSearch is disabled',
|
|
'Desktop search will not run after Daily Set. Enable doDesktopSearch or disable bundling.'
|
|
)
|
|
}
|
|
}
|
|
|
|
private validateExecutionSettings(config: Config): void {
|
|
// Validate passesPerRun
|
|
const passes = config.passesPerRun ?? 1
|
|
|
|
if (passes < 1) {
|
|
this.addError(
|
|
'execution',
|
|
'passesPerRun must be at least 1',
|
|
'Set passesPerRun to 1 or higher in config.jsonc',
|
|
undefined,
|
|
true
|
|
)
|
|
}
|
|
|
|
if (passes > 5) {
|
|
this.addWarning(
|
|
'execution',
|
|
`passesPerRun is set to ${passes} (very high)`,
|
|
'Running multiple passes per day may trigger Microsoft detection. Recommended: 1-2 passes max',
|
|
'docs/config-reference.md'
|
|
)
|
|
}
|
|
|
|
if (passes > 1) {
|
|
// This is intentional behavior confirmation, not a warning
|
|
log('main', 'VALIDATION', `✓ [OK] passesPerRun = ${passes}: Job-state skip is disabled (intentional)`)
|
|
log('main', 'VALIDATION', ' Suggestion: All accounts will run on every pass, even if already completed. This is intentional for multiple passes.')
|
|
log('main', 'VALIDATION', ' Docs: docs/jobstate.md')
|
|
}
|
|
|
|
// Validate clusters
|
|
if (config.clusters < 1) {
|
|
this.addError(
|
|
'execution',
|
|
'clusters must be at least 1',
|
|
'Set clusters to 1 or higher in config.jsonc',
|
|
undefined,
|
|
true
|
|
)
|
|
}
|
|
|
|
if (config.clusters > 10) {
|
|
this.addWarning(
|
|
'execution',
|
|
`clusters is set to ${config.clusters} (very high)`,
|
|
'Too many clusters may cause resource exhaustion. Recommended: 1-4 clusters'
|
|
)
|
|
}
|
|
}
|
|
|
|
private validateSearchSettings(config: Config): void {
|
|
const search = config.searchSettings
|
|
|
|
// Retry validation
|
|
if (search.retryMobileSearchAmount < 0) {
|
|
this.addWarning(
|
|
'search',
|
|
'retryMobileSearchAmount is negative',
|
|
'Set to 0 or positive number (recommended: 2-3)'
|
|
)
|
|
}
|
|
|
|
if (search.retryMobileSearchAmount > 10) {
|
|
this.addWarning(
|
|
'search',
|
|
`retryMobileSearchAmount is very high (${search.retryMobileSearchAmount})`,
|
|
'High retry count may trigger detection. Recommended: 2-3'
|
|
)
|
|
}
|
|
|
|
// Fallback validation
|
|
if (search.localFallbackCount !== undefined && search.localFallbackCount < 10) {
|
|
this.addWarning(
|
|
'search',
|
|
`localFallbackCount is low (${search.localFallbackCount})`,
|
|
'Consider at least 15-25 fallback queries for variety'
|
|
)
|
|
}
|
|
|
|
// Query diversity check
|
|
if (config.queryDiversity?.enabled === false && !config.searchOnBingLocalQueries) {
|
|
this.addWarning(
|
|
'search',
|
|
'Query diversity disabled and local queries disabled',
|
|
'Bot will only use Google Trends. Enable one query source for better variety.',
|
|
'docs/config.md'
|
|
)
|
|
}
|
|
}
|
|
|
|
private validateHumanizationSettings(config: Config): void {
|
|
const human = config.humanization
|
|
|
|
if (!human || human.enabled === false) {
|
|
this.addWarning(
|
|
'humanization',
|
|
'Humanization is completely disabled',
|
|
'This increases detection risk. Consider enabling for safer automation.',
|
|
'docs/config.md'
|
|
)
|
|
return
|
|
}
|
|
|
|
// Gesture probabilities
|
|
if (human.gestureMoveProb !== undefined) {
|
|
if (human.gestureMoveProb < 0 || human.gestureMoveProb > 1) {
|
|
this.addError(
|
|
'humanization',
|
|
`gestureMoveProb must be between 0 and 1 (got ${human.gestureMoveProb})`,
|
|
'Set a probability value between 0.0 and 1.0'
|
|
)
|
|
}
|
|
if (human.gestureMoveProb === 0) {
|
|
this.addWarning(
|
|
'humanization',
|
|
'Mouse gestures disabled (gestureMoveProb=0)',
|
|
'This may look robotic. Consider 0.3-0.7 for natural behavior.'
|
|
)
|
|
}
|
|
}
|
|
|
|
if (human.gestureScrollProb !== undefined) {
|
|
if (human.gestureScrollProb < 0 || human.gestureScrollProb > 1) {
|
|
this.addError(
|
|
'humanization',
|
|
`gestureScrollProb must be between 0 and 1 (got ${human.gestureScrollProb})`,
|
|
'Set a probability value between 0.0 and 1.0'
|
|
)
|
|
}
|
|
}
|
|
|
|
// Action delays
|
|
if (human.actionDelay) {
|
|
const minMs = typeof human.actionDelay.min === 'string'
|
|
? parseInt(human.actionDelay.min, 10)
|
|
: human.actionDelay.min
|
|
const maxMs = typeof human.actionDelay.max === 'string'
|
|
? parseInt(human.actionDelay.max, 10)
|
|
: human.actionDelay.max
|
|
|
|
if (minMs > maxMs) {
|
|
this.addError(
|
|
'humanization',
|
|
'actionDelay min is greater than max',
|
|
`Fix: min=${minMs} should be <= max=${maxMs}`
|
|
)
|
|
}
|
|
}
|
|
|
|
// Random off days
|
|
if (human.randomOffDaysPerWeek !== undefined) {
|
|
if (human.randomOffDaysPerWeek < 0 || human.randomOffDaysPerWeek > 7) {
|
|
this.addError(
|
|
'humanization',
|
|
`randomOffDaysPerWeek must be 0-7 (got ${human.randomOffDaysPerWeek})`,
|
|
'Set to a value between 0 (no off days) and 7 (always off)'
|
|
)
|
|
}
|
|
}
|
|
|
|
// Allowed windows validation
|
|
if (human.allowedWindows && Array.isArray(human.allowedWindows)) {
|
|
human.allowedWindows.forEach((window, idx) => {
|
|
if (typeof window !== 'string') {
|
|
this.addError(
|
|
'humanization',
|
|
`allowedWindows[${idx}] is not a string`,
|
|
'Format: "HH:mm-HH:mm" (e.g., "09:00-17:00")'
|
|
)
|
|
} else if (!/^\d{2}:\d{2}-\d{2}:\d{2}$/.test(window)) {
|
|
this.addWarning(
|
|
'humanization',
|
|
`allowedWindows[${idx}] format may be invalid: "${window}"`,
|
|
'Expected format: "HH:mm-HH:mm" (24-hour, e.g., "09:00-17:00")'
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private validateSecuritySettings(config: Config): void {
|
|
// Check logging redaction
|
|
const logging = config.logging as { redactEmails?: boolean } | undefined
|
|
if (logging && logging.redactEmails === false) {
|
|
this.addWarning(
|
|
'security',
|
|
'Email redaction is disabled in logs',
|
|
'Enable redactEmails=true if you share logs publicly',
|
|
'docs/security.md'
|
|
)
|
|
}
|
|
|
|
// Proxy exposure check
|
|
if (config.proxy?.proxyGoogleTrends === false && config.proxy?.proxyBingTerms === false) {
|
|
this.addWarning(
|
|
'security',
|
|
'All external API calls will use your real IP',
|
|
'Consider enabling proxy for Google Trends or Bing Terms to mask your IP'
|
|
)
|
|
}
|
|
|
|
// Crash recovery
|
|
if (config.crashRecovery?.autoRestart === true) {
|
|
const maxRestarts = config.crashRecovery.maxRestarts ?? 2
|
|
if (maxRestarts > 5) {
|
|
this.addWarning(
|
|
'security',
|
|
`Crash recovery maxRestarts is high (${maxRestarts})`,
|
|
'Excessive restarts on errors may trigger rate limits or detection'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private addError(category: string, message: string, fix?: string, docsLink?: string, blocking = false): void {
|
|
this.errors.push({ severity: 'error', category, message, fix, docsLink, blocking })
|
|
}
|
|
|
|
private addWarning(category: string, message: string, fix?: string, docsLink?: string): void {
|
|
this.warnings.push({ severity: 'warning', category, message, fix, docsLink, blocking: false })
|
|
}
|
|
|
|
private async displayResults(): Promise<void> {
|
|
if (this.errors.length > 0) {
|
|
log('main', 'VALIDATION', chalk.red('❌ VALIDATION ERRORS FOUND:'), 'error')
|
|
this.errors.forEach((err, index) => {
|
|
log('main', 'VALIDATION', chalk.red(`${index + 1}. [${err.category.toUpperCase()}] ${err.message}`), 'error')
|
|
if (err.fix) {
|
|
log('main', 'VALIDATION', chalk.yellow(` Fix: ${err.fix}`), 'warn')
|
|
}
|
|
if (err.docsLink) {
|
|
log('main', 'VALIDATION', ` Docs: ${err.docsLink}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (this.warnings.length > 0) {
|
|
log('main', 'VALIDATION', chalk.yellow('⚠️ WARNINGS:'), 'warn')
|
|
this.warnings.forEach((warn, index) => {
|
|
log('main', 'VALIDATION', chalk.yellow(`${index + 1}. [${warn.category.toUpperCase()}] ${warn.message}`), 'warn')
|
|
if (warn.fix) {
|
|
log('main', 'VALIDATION', ` Suggestion: ${warn.fix}`)
|
|
}
|
|
if (warn.docsLink) {
|
|
log('main', 'VALIDATION', ` Docs: ${warn.docsLink}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (this.errors.length === 0 && this.warnings.length === 0) {
|
|
log('main', 'VALIDATION', chalk.green('✅ All validation checks passed!'))
|
|
} else {
|
|
const errorLabel = this.errors.length === 1 ? 'error' : 'errors'
|
|
const warningLabel = this.warnings.length === 1 ? 'warning' : 'warnings'
|
|
log('main', 'VALIDATION', `[${this.errors.length > 0 ? 'ERROR' : 'OK'}] Found: ${this.errors.length} ${errorLabel} | ${this.warnings.length} ${warningLabel}`)
|
|
|
|
if (this.errors.length > 0) {
|
|
log('main', 'VALIDATION', 'Bot will continue, but issues may cause failures', 'warn')
|
|
log('main', 'VALIDATION', 'Full documentation: docs/index.md')
|
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
} else if (this.warnings.length > 0) {
|
|
log('main', 'VALIDATION', 'Warnings detected - review recommended', 'warn')
|
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
}
|
|
}
|
|
}
|
|
}
|