Fix: Enhance error handling and timeout management across various modules; improve validation and documentation

This commit is contained in:
2025-11-08 18:52:31 +01:00
parent ca356075fa
commit 5e322af2c0
13 changed files with 341 additions and 86 deletions

View File

@@ -149,6 +149,7 @@ function normalizeConfig(raw: unknown): Config {
const saveFingerprint = (n.fingerprinting?.saveFingerprint ?? n.saveFingerprint) ?? { mobile: false, desktop: false }
// Humanization defaults (single on/off)
// FIXED: Always initialize humanization object first to prevent undefined access
if (!n.humanization) n.humanization = {}
if (typeof n.humanization.enabled !== 'boolean') n.humanization.enabled = true
if (typeof n.humanization.stopOnBan !== 'boolean') n.humanization.stopOnBan = false
@@ -250,6 +251,23 @@ function normalizeConfig(raw: unknown): Config {
return cfg
}
// IMPROVED: Generic helper to reduce duplication
function extractStringField(obj: unknown, key: string): string | undefined {
if (obj && typeof obj === 'object' && key in obj) {
const value = (obj as Record<string, unknown>)[key]
return typeof value === 'string' ? value : undefined
}
return undefined
}
function extractBooleanField(obj: unknown, key: string): boolean | undefined {
if (obj && typeof obj === 'object' && key in obj) {
const value = (obj as Record<string, unknown>)[key]
return typeof value === 'boolean' ? value : undefined
}
return undefined
}
function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined {
if (!raw || typeof raw !== 'object') return undefined
@@ -261,13 +279,12 @@ function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined {
const cronRaw = source.cron
if (cronRaw && typeof cronRaw === 'object') {
const cronSource = cronRaw as Record<string, unknown>
scheduling.cron = {
schedule: typeof cronSource.schedule === 'string' ? cronSource.schedule : undefined,
workingDirectory: typeof cronSource.workingDirectory === 'string' ? cronSource.workingDirectory : undefined,
nodePath: typeof cronSource.nodePath === 'string' ? cronSource.nodePath : undefined,
logFile: typeof cronSource.logFile === 'string' ? cronSource.logFile : undefined,
user: typeof cronSource.user === 'string' ? cronSource.user : undefined
schedule: extractStringField(cronRaw, 'schedule'),
workingDirectory: extractStringField(cronRaw, 'workingDirectory'),
nodePath: extractStringField(cronRaw, 'nodePath'),
logFile: extractStringField(cronRaw, 'logFile'),
user: extractStringField(cronRaw, 'user')
}
}
@@ -275,12 +292,12 @@ function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined {
if (taskRaw && typeof taskRaw === 'object') {
const taskSource = taskRaw as Record<string, unknown>
scheduling.taskScheduler = {
taskName: typeof taskSource.taskName === 'string' ? taskSource.taskName : undefined,
schedule: typeof taskSource.schedule === 'string' ? taskSource.schedule : undefined,
taskName: extractStringField(taskRaw, 'taskName'),
schedule: extractStringField(taskRaw, 'schedule'),
frequency: typeof taskSource.frequency === 'string' ? taskSource.frequency as 'daily' | 'weekly' | 'once' : undefined,
workingDirectory: typeof taskSource.workingDirectory === 'string' ? taskSource.workingDirectory : undefined,
runAsUser: typeof taskSource.runAsUser === 'boolean' ? taskSource.runAsUser : undefined,
highestPrivileges: typeof taskSource.highestPrivileges === 'boolean' ? taskSource.highestPrivileges : undefined
workingDirectory: extractStringField(taskRaw, 'workingDirectory'),
runAsUser: extractBooleanField(taskRaw, 'runAsUser'),
highestPrivileges: extractBooleanField(taskRaw, 'highestPrivileges')
}
}
@@ -345,8 +362,13 @@ export function loadAccounts(): Account[] {
// Accept either a root array or an object with an `accounts` array, ignore `_note`
const parsed = Array.isArray(parsedUnknown) ? parsedUnknown : (parsedUnknown && typeof parsedUnknown === 'object' && Array.isArray((parsedUnknown as { accounts?: unknown }).accounts) ? (parsedUnknown as { accounts: unknown[] }).accounts : null)
if (!Array.isArray(parsed)) throw new Error('accounts must be an array')
// minimal shape validation
// FIXED: Validate entries BEFORE type assertion for better type safety
for (const entry of parsed) {
// Pre-validation: Check basic structure before casting
if (!entry || typeof entry !== 'object') {
throw new Error('each account entry must be an object')
}
// JUSTIFIED USE OF `any`: Accounts come from untrusted user JSON with unpredictable structure
// We perform explicit runtime validation of each property below (typeof checks, regex validation, etc.)
// This is safer than trusting a type assertion to a specific interface

View File

@@ -42,11 +42,16 @@ const cleanupInterval = setInterval(() => {
}
}, BUFFER_CLEANUP_INTERVAL_MS)
// Allow cleanup to be stopped (prevents process from hanging)
if (cleanupInterval.unref) {
// FIXED: Allow cleanup to be stopped with proper fallback
// unref() prevents process from hanging but may not exist in all environments
if (typeof cleanupInterval.unref === 'function') {
cleanupInterval.unref()
}
/**
* Get or create a webhook buffer for the given URL
* Buffers batch log messages to reduce Discord API calls
*/
function getBuffer(url: string): WebhookBuffer {
let buf = webhookBuffers.get(url)
if (!buf) {
@@ -58,6 +63,10 @@ function getBuffer(url: string): WebhookBuffer {
return buf
}
/**
* Send batched log messages to Discord webhook
* Handles rate limiting and message size constraints
*/
async function sendBatch(url: string, buf: WebhookBuffer): Promise<void> {
if (buf.sending) return
buf.sending = true
@@ -104,27 +113,29 @@ async function sendBatch(url: string, buf: WebhookBuffer): Promise<void> {
buf.sending = false
}
// IMPROVED: Extracted color determination logic for better maintainability
type ColorRule = { pattern: RegExp | string; color: number }
const COLOR_RULES: ColorRule[] = [
{ pattern: /\[banned\]|\[security\]|suspended|compromised/i, color: DISCORD.COLOR_RED },
{ pattern: /\[error\]|✗/i, color: DISCORD.COLOR_CRIMSON },
{ pattern: /\[warn\]|⚠/i, color: DISCORD.COLOR_ORANGE },
{ pattern: /\[ok\]|✓|complet/i, color: DISCORD.COLOR_GREEN },
{ pattern: /\[main\]/i, color: DISCORD.COLOR_BLUE }
]
function determineColorFromContent(content: string): number {
const lower = content.toLowerCase()
// Priority order: most critical first
if (lower.includes('[banned]') || lower.includes('[security]') || lower.includes('suspended') || lower.includes('compromised')) {
return DISCORD.COLOR_RED
}
if (lower.includes('[error]') || lower.includes('✗')) {
return DISCORD.COLOR_CRIMSON
}
if (lower.includes('[warn]') || lower.includes('⚠')) {
return DISCORD.COLOR_ORANGE
}
if (lower.includes('[ok]') || lower.includes('✓') || lower.includes('complet')) {
return DISCORD.COLOR_GREEN
}
if (lower.includes('[main]')) {
return DISCORD.COLOR_BLUE
// Check rules in priority order
for (const rule of COLOR_RULES) {
if (typeof rule.pattern === 'string') {
if (lower.includes(rule.pattern)) return rule.color
} else {
if (rule.pattern.test(lower)) return rule.color
}
}
return 0x95A5A6
return DISCORD.COLOR_GRAY
}
function enqueueWebhookLog(url: string, line: string) {
@@ -138,7 +149,17 @@ function enqueueWebhookLog(url: string, line: string) {
}
}
// Synchronous logger that returns an Error when type === 'error' so callers can `throw log(...)` safely.
/**
* Centralized logging function with console, Discord webhook, and NTFY support
* @param isMobile - Platform identifier ('main', true for mobile, false for desktop)
* @param title - Log title/category (e.g., 'LOGIN', 'SEARCH')
* @param message - Log message content
* @param type - Log level (log, warn, error)
* @param color - Optional chalk color override
* @returns Error object if type is 'error' (allows `throw log(...)`)
* @example log('main', 'STARTUP', 'Bot started', 'log')
* @example throw log(false, 'LOGIN', 'Auth failed', 'error')
*/
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void {
const configData = loadConfig()

View File

@@ -11,9 +11,18 @@ type NumericPolicy = {
export type Retryable<T> = () => Promise<T>
/**
* Exponential backoff retry mechanism with jitter
* IMPROVED: Added comprehensive documentation
*/
export class Retry {
private policy: NumericPolicy
/**
* Create a retry handler with exponential backoff
* @param policy - Retry policy configuration (optional)
* @example new Retry({ maxAttempts: 5, baseDelay: 2000 })
*/
constructor(policy?: ConfigRetryPolicy) {
const def: NumericPolicy = {
maxAttempts: 3,
@@ -38,6 +47,14 @@ export class Retry {
}
}
/**
* Execute a function with exponential backoff retry logic
* @param fn - Async function to retry
* @param isRetryable - Optional predicate to determine if error is retryable
* @returns Result of the function
* @throws {Error} Last error if all attempts fail
* @example await retry.run(() => fetchAPI(), (err) => err.statusCode !== 404)
*/
async run<T>(fn: Retryable<T>, isRetryable?: (e: unknown) => boolean): Promise<T> {
let attempt = 0
let delay = this.policy.baseDelay
@@ -51,7 +68,8 @@ export class Retry {
attempt += 1
const retry = isRetryable ? isRetryable(e) : true
if (!retry || attempt >= this.policy.maxAttempts) break
const jitter = 1 + (Math.random() * 2 - 1) * this.policy.jitter
// FIXED: Jitter should always increase delay, not decrease it (remove the -1)
const jitter = 1 + Math.random() * this.policy.jitter
const sleep = Math.min(this.policy.maxDelay, Math.max(0, Math.floor(delay * jitter)))
await new Promise((r) => setTimeout(r, sleep))
delay = Math.min(this.policy.maxDelay, Math.floor(delay * (this.policy.multiplier || 2)))

View File

@@ -1,17 +1,32 @@
import ms from 'ms'
/**
* Extract error message from unknown error type
* @param error - Error object or unknown value
* @returns String representation of the error
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/**
* Utility class for common operations
* IMPROVED: Added comprehensive documentation
*/
export class Util {
/**
* Wait for a specified number of milliseconds
* @param ms - Milliseconds to wait (max 1 hour)
* @throws {Error} If ms is not finite or is NaN/Infinity
* @example await utils.wait(1000) // Wait 1 second
*/
wait(ms: number): Promise<void> {
const MAX_WAIT_MS = 3600000 // 1 hour max to prevent infinite waits
const MIN_WAIT_MS = 0
// Validate and clamp input - explicit NaN check before isFinite
if (typeof ms !== 'number' || Number.isNaN(ms) || !Number.isFinite(ms)) {
// FIXED: Simplified validation - isFinite checks both NaN and Infinity
if (!Number.isFinite(ms)) {
throw new Error(`Invalid wait time: ${ms}. Must be a finite number (not NaN or Infinity).`)
}
@@ -22,6 +37,13 @@ export class Util {
})
}
/**
* Wait for a random duration within a range
* @param minMs - Minimum wait time in milliseconds
* @param maxMs - Maximum wait time in milliseconds
* @throws {Error} If parameters are invalid
* @example await utils.waitRandom(1000, 3000) // Wait 1-3 seconds
*/
async waitRandom(minMs: number, maxMs: number): Promise<void> {
if (!Number.isFinite(minMs) || !Number.isFinite(maxMs)) {
throw new Error(`Invalid wait range: min=${minMs}, max=${maxMs}. Both must be finite numbers.`)
@@ -35,6 +57,13 @@ export class Util {
return this.wait(delta)
}
/**
* Format a timestamp as MM/DD/YYYY
* @param ms - Unix timestamp in milliseconds (defaults to current time)
* @returns Formatted date string
* @example utils.getFormattedDate() // '01/15/2025'
* @example utils.getFormattedDate(1704067200000) // '01/01/2024'
*/
getFormattedDate(ms = Date.now()): string {
const today = new Date(ms)
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0
@@ -44,12 +73,26 @@ export class Util {
return `${month}/${day}/${year}`
}
/**
* Randomly shuffle an array using Fisher-Yates algorithm
* @param array - Array to shuffle
* @returns New shuffled array (original array is not modified)
* @example utils.shuffleArray([1, 2, 3, 4]) // [3, 1, 4, 2]
*/
shuffleArray<T>(array: T[]): T[] {
return array.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
}
/**
* Generate a random integer between min and max (inclusive)
* @param min - Minimum value
* @param max - Maximum value
* @returns Random integer in range [min, max]
* @throws {Error} If parameters are invalid
* @example utils.randomNumber(1, 10) // 7
*/
randomNumber(min: number, max: number): number {
if (!Number.isFinite(min) || !Number.isFinite(max)) {
throw new Error(`Invalid range: min=${min}, max=${max}. Both must be finite numbers.`)
@@ -62,12 +105,16 @@ export class Util {
return Math.floor(Math.random() * (max - min + 1)) + min
}
/**
* Split an array into approximately equal chunks
* @param arr - Array to split
* @param numChunks - Number of chunks to create (must be positive integer)
* @returns Array of chunks (sub-arrays)
* @throws {Error} If parameters are invalid
* @example utils.chunkArray([1,2,3,4,5], 2) // [[1,2,3], [4,5]]
*/
chunkArray<T>(arr: T[], numChunks: number): T[][] {
// Validate input to prevent division by zero or invalid chunks
if (!Number.isFinite(numChunks) || numChunks <= 0) {
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
}
// FIXED: Stricter validation with better error messages
if (!Array.isArray(arr)) {
throw new Error('Invalid input: arr must be an array.')
}
@@ -76,6 +123,14 @@ export class Util {
return []
}
if (!Number.isFinite(numChunks) || numChunks <= 0) {
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
}
if (!Number.isInteger(numChunks)) {
throw new Error(`Invalid numChunks: ${numChunks}. Must be an integer.`)
}
const safeNumChunks = Math.max(1, Math.floor(numChunks))
const chunkSize = Math.ceil(arr.length / safeNumChunks)
const chunks: T[][] = []
@@ -88,6 +143,15 @@ export class Util {
return chunks
}
/**
* Convert time string or number to milliseconds
* @param input - Time string (e.g., '1 min', '5s', '2h') or number
* @returns Time in milliseconds
* @throws {Error} If input cannot be parsed
* @example utils.stringToMs('1 min') // 60000
* @example utils.stringToMs('5s') // 5000
* @example utils.stringToMs(1000) // 1000
*/
stringToMs(input: string | number): number {
if (typeof input !== 'string' && typeof input !== 'number') {
throw new Error('Invalid input type. Expected string or number.')