style: auto-fix lint errors and formatting issues

Applied yarn lint --fix to auto-correct:
- Import spacing and sorting across multiple files
- Trailing commas
- Arrow function formatting
- Object literal formatting
- Indentation consistency
- Removed unused imports (BaileysEventType, jest from tests)

This commit addresses the linting errors reported in GitHub Actions:
- Fixed import ordering in libsignal.ts
- Fixed trailing commas in Defaults/index.ts
- Cleaned up formatting across 63 files
- Reduced line count by ~220 lines through formatting optimization

Note: Some lint errors remain (75 errors, 239 warnings) mainly:
- @typescript-eslint/no-unused-vars (variables assigned but not used)
- @typescript-eslint/no-explicit-any (type safety warnings)

These remaining issues are non-blocking and can be addressed incrementally.

https://claude.ai/code/session_015R3U3kiprQiNTTNNt31Sg6
This commit is contained in:
Claude
2026-02-13 21:59:35 +00:00
parent ce98b240ca
commit 4b02652369
63 changed files with 1677 additions and 1897 deletions
+71 -46
View File
@@ -14,8 +14,8 @@
*/
import { EventEmitter } from 'events'
import { metrics } from './prometheus-metrics.js'
import type { BaileysLogCategory } from './baileys-logger.js'
import { metrics } from './prometheus-metrics.js'
/**
* Baileys event types
@@ -56,7 +56,7 @@ const PRIORITY_VALUES: Record<EventPriority, number> = {
critical: 0,
high: 1,
normal: 2,
low: 3,
low: 3
}
/**
@@ -160,9 +160,9 @@ const EVENT_CATEGORY_MAP: Record<string, BaileysLogCategory> = {
'presence.update': 'presence',
'chats.update': 'message',
'chats.delete': 'message',
'call': 'call',
call: 'call',
'blocklist.set': 'sync',
'blocklist.update': 'sync',
'blocklist.update': 'sync'
}
/**
@@ -173,9 +173,9 @@ const EVENT_PRIORITY_MAP: Partial<Record<BaileysEventType, EventPriority>> = {
'creds.update': 'critical',
'messages.upsert': 'high',
'messages.update': 'high',
'call': 'high',
call: 'high',
'presence.update': 'low',
'messaging-history.set': 'normal',
'messaging-history.set': 'normal'
}
/**
@@ -213,7 +213,7 @@ export class BaileysEventStream extends EventEmitter {
maxRetries: options.maxRetries ?? 3,
deadLetterQueueSize: options.deadLetterQueueSize ?? 1000,
collectMetrics: options.collectMetrics ?? true,
streamName: options.streamName ?? 'baileys',
streamName: options.streamName ?? 'baileys'
}
this.stats = this.createInitialStats()
@@ -238,15 +238,19 @@ export class BaileysEventStream extends EventEmitter {
critical: 0,
high: 0,
normal: 0,
low: 0,
},
low: 0
}
}
}
/**
* Add event to stream
*/
push<T>(type: BaileysEventType, data: T, options?: { priority?: EventPriority; metadata?: Record<string, unknown> }): boolean {
push<T>(
type: BaileysEventType,
data: T,
options?: { priority?: EventPriority; metadata?: Record<string, unknown> }
): boolean {
// Check backpressure
if (this.options.enableBackpressure && this.buffer.length >= this.options.highWaterMark) {
this.stats.isBackpressured = true
@@ -272,7 +276,7 @@ export class BaileysEventStream extends EventEmitter {
priority: options?.priority || EVENT_PRIORITY_MAP[type] || 'normal',
category: EVENT_CATEGORY_MAP[type] || 'unknown',
metadata: options?.metadata,
retryCount: 0,
retryCount: 0
}
// Apply transformers
@@ -334,9 +338,18 @@ export class BaileysEventStream extends EventEmitter {
/**
* Register handler for event type
*/
on<T = unknown>(event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', handler: EventHandler<T>): this {
on<T = unknown>(
event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry',
handler: EventHandler<T>
): this {
// For control events (backpressure, drain, etc), use native EventEmitter
if (event === 'backpressure' || event === 'drain' || event === 'dropped' || event === 'batch-processed' || event === 'retry') {
if (
event === 'backpressure' ||
event === 'drain' ||
event === 'dropped' ||
event === 'batch-processed' ||
event === 'retry'
) {
super.on(event, handler as any)
return this
}
@@ -345,6 +358,7 @@ export class BaileysEventStream extends EventEmitter {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set())
}
this.handlers.get(event)!.add(handler as EventHandler)
return this
}
@@ -352,9 +366,18 @@ export class BaileysEventStream extends EventEmitter {
/**
* Remove handler
*/
off(event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', handler: EventHandler): this {
off(
event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry',
handler: EventHandler
): this {
// For control events, use native EventEmitter
if (event === 'backpressure' || event === 'drain' || event === 'dropped' || event === 'batch-processed' || event === 'retry') {
if (
event === 'backpressure' ||
event === 'drain' ||
event === 'dropped' ||
event === 'batch-processed' ||
event === 'retry'
) {
super.off(event, handler as any)
return this
}
@@ -364,6 +387,7 @@ export class BaileysEventStream extends EventEmitter {
if (handlers) {
handlers.delete(handler)
}
return this
}
@@ -371,10 +395,11 @@ export class BaileysEventStream extends EventEmitter {
* Register single-use handler
*/
once<T = unknown>(event: BaileysEventType, handler: EventHandler<T>): this {
const wrappedHandler: EventHandler<T> = (e) => {
const wrappedHandler: EventHandler<T> = e => {
this.off(event, wrappedHandler as EventHandler)
return handler(e)
}
return this.on(event, wrappedHandler)
}
@@ -394,6 +419,7 @@ export class BaileysEventStream extends EventEmitter {
if (index !== -1) {
this.filters.splice(index, 1)
}
return this
}
@@ -510,8 +536,8 @@ export class BaileysEventStream extends EventEmitter {
...event.metadata,
error: error.message,
errorStack: error.stack,
movedToDlqAt: Date.now(),
},
movedToDlqAt: Date.now()
}
}
this.deadLetterQueue.push(dlqEvent)
@@ -554,7 +580,7 @@ export class BaileysEventStream extends EventEmitter {
return {
processed,
failed,
duration: Date.now() - startTime,
duration: Date.now() - startTime
}
}
@@ -636,7 +662,7 @@ export class BaileysEventStream extends EventEmitter {
return {
processed,
failed,
duration: Date.now() - startTime,
duration: Date.now() - startTime
}
}
@@ -663,6 +689,7 @@ export class BaileysEventStream extends EventEmitter {
if (this.flushTimer) {
clearInterval(this.flushTimer)
}
this.buffer = []
this.deadLetterQueue = []
this.handlers.clear()
@@ -686,38 +713,38 @@ export const eventFilters = {
/** Filter by event type */
byType:
(...types: BaileysEventType[]): EventFilter =>
(event) =>
types.includes(event.type),
event =>
types.includes(event.type),
/** Filter by category */
byCategory:
(...categories: BaileysLogCategory[]): EventFilter =>
(event) =>
categories.includes(event.category),
event =>
categories.includes(event.category),
/** Filter by minimum priority */
byMinPriority:
(minPriority: EventPriority): EventFilter =>
(event) =>
PRIORITY_VALUES[event.priority] <= PRIORITY_VALUES[minPriority],
event =>
PRIORITY_VALUES[event.priority] <= PRIORITY_VALUES[minPriority],
/** Filter recent events (within ms) */
recentOnly:
(maxAgeMs: number): EventFilter =>
(event) =>
Date.now() - event.timestamp <= maxAgeMs,
event =>
Date.now() - event.timestamp <= maxAgeMs,
/** Combine filters with AND */
and:
(...filters: EventFilter[]): EventFilter =>
(event) =>
filters.every((f) => f(event)),
event =>
filters.every(f => f(event)),
/** Combine filters with OR */
or:
(...filters: EventFilter[]): EventFilter =>
(event) =>
filters.some((f) => f(event)),
event =>
filters.some(f => f(event))
}
/**
@@ -725,32 +752,30 @@ export const eventFilters = {
*/
export const eventTransformers = {
/** Add processing timestamp */
addProcessingTimestamp: (): EventTransformer => (event) => ({
addProcessingTimestamp: (): EventTransformer => event => ({
...event,
metadata: {
...event.metadata,
processingTimestamp: Date.now(),
},
processingTimestamp: Date.now()
}
}),
/** Add trace ID */
addTraceId:
(traceIdGenerator: () => string): EventTransformer =>
(event) => ({
...event,
metadata: {
...event.metadata,
traceId: traceIdGenerator(),
},
}),
event => ({
...event,
metadata: {
...event.metadata,
traceId: traceIdGenerator()
}
}),
/** Elevate priority based on condition */
elevatepriorityIf:
(condition: (event: StreamEvent) => boolean, newPriority: EventPriority): EventTransformer =>
(event) =>
condition(event)
? { ...event, priority: newPriority }
: event,
event =>
condition(event) ? { ...event, priority: newPriority } : event
}
export default BaileysEventStream
+58 -64
View File
@@ -12,25 +12,25 @@
*/
import type { ILogger } from './logger.js'
import { StructuredLogger, createStructuredLogger, type LogLevel, type LogEntry } from './structured-logger.js'
import { createStructuredLogger, type LogEntry, type LogLevel, StructuredLogger } from './structured-logger.js'
/**
* Baileys-specific log categories
*/
export type BaileysLogCategory =
| 'connection' // WebSocket connection events
| 'auth' // Authentication and QR code
| 'message' // Message send/receive
| 'media' // Media upload/download
| 'group' // Group operations
| 'presence' // Presence status
| 'call' // Voice/video calls
| 'sync' // Data synchronization
| 'encryption' // Encryption operations
| 'retry' // Operation retries
| 'socket' // Low-level socket events
| 'binary' // Binary encoding/decoding
| 'unknown' // Unknown category
| 'connection' // WebSocket connection events
| 'auth' // Authentication and QR code
| 'message' // Message send/receive
| 'media' // Media upload/download
| 'group' // Group operations
| 'presence' // Presence status
| 'call' // Voice/video calls
| 'sync' // Data synchronization
| 'encryption' // Encryption operations
| 'retry' // Operation retries
| 'socket' // Low-level socket events
| 'binary' // Binary encoding/decoding
| 'unknown' // Unknown category
/**
* Baileys Logger configuration
@@ -86,7 +86,7 @@ const CATEGORY_PATTERNS: Array<{ pattern: RegExp; category: BaileysLogCategory }
{ pattern: /sync|history|initial|full/i, category: 'sync' },
{ pattern: /encrypt|decrypt|signal|key|cipher/i, category: 'encryption' },
{ pattern: /retry|attempt|backoff|reconnect/i, category: 'retry' },
{ pattern: /binary|encode|decode|proto|buffer/i, category: 'binary' },
{ pattern: /binary|encode|decode|proto|buffer/i, category: 'binary' }
]
/**
@@ -118,14 +118,14 @@ export class BaileysLogger implements ILogger {
logBinaryData: config.logBinaryData ?? false,
instanceId: config.instanceId || this.generateInstanceId(),
eventHandler: config.eventHandler || (() => {}),
maxPayloadSize: config.maxPayloadSize || 1024,
maxPayloadSize: config.maxPayloadSize || 1024
}
this.structuredLogger = createStructuredLogger({
level: this.config.level,
name: `baileys:${this.config.instanceId}`,
jsonFormat: process.env.NODE_ENV === 'production',
redactFields: ['password', 'token', 'secret', 'key', 'authKey', 'macKey'],
redactFields: ['password', 'token', 'secret', 'key', 'authKey', 'macKey']
})
this.metrics = this.createInitialMetrics()
@@ -159,8 +159,8 @@ export class BaileysLogger implements ILogger {
retry: 0,
socket: 0,
binary: 0,
unknown: 0,
},
unknown: 0
}
}
}
@@ -189,7 +189,7 @@ export class BaileysLogger implements ILogger {
const searchText = [
msg || '',
typeof obj === 'string' ? obj : '',
typeof obj === 'object' && obj !== null ? JSON.stringify(obj) : '',
typeof obj === 'object' && obj !== null ? JSON.stringify(obj) : ''
].join(' ')
for (const { pattern, category } of CATEGORY_PATTERNS) {
@@ -249,7 +249,7 @@ export class BaileysLogger implements ILogger {
return {
_truncated: true,
_originalSize: str.length,
_preview: str.substring(0, 200) + '...',
_preview: str.substring(0, 200) + '...'
}
}
}
@@ -273,6 +273,7 @@ export class BaileysLogger implements ILogger {
} else if (/fail|error|close/i.test(objStr)) {
this.metrics.connectionFailures++
}
break
case 'message':
@@ -283,6 +284,7 @@ export class BaileysLogger implements ILogger {
this.metrics.messagesReceived++
this.metrics.lastMessageTime = new Date().toISOString()
}
break
case 'media':
@@ -291,6 +293,7 @@ export class BaileysLogger implements ILogger {
} else if (/download/i.test(objStr)) {
this.metrics.mediaDownloads++
}
break
case 'retry':
@@ -328,12 +331,14 @@ export class BaileysLogger implements ILogger {
category,
instanceId: this.config.instanceId,
...this.childContext,
...(typeof sanitizedObj === 'object' && sanitizedObj !== null ? sanitizedObj : { value: sanitizedObj }),
...(typeof sanitizedObj === 'object' && sanitizedObj !== null ? sanitizedObj : { value: sanitizedObj })
}
// Structured log (skip if level is 'silent')
if (level !== 'silent') {
const logMethod = (this.structuredLogger as unknown as Record<string, ((obj: unknown, msg?: string) => void) | undefined>)[level]
const logMethod = (
this.structuredLogger as unknown as Record<string, ((obj: unknown, msg?: string) => void) | undefined>
)[level]
if (logMethod) {
logMethod.call(this.structuredLogger, enrichedObj, msg)
}
@@ -347,7 +352,7 @@ export class BaileysLogger implements ILogger {
levelValue: 0,
message: msg || '',
name: `baileys:${this.config.instanceId}`,
data: enrichedObj,
data: enrichedObj
}
this.config.eventHandler(category, entry)
}
@@ -384,12 +389,7 @@ export class BaileysLogger implements ILogger {
/**
* Log message-specific event
*/
logMessage(
direction: 'send' | 'receive',
messageType: string,
jid: string,
details?: Record<string, unknown>
): void {
logMessage(direction: 'send' | 'receive', messageType: string, jid: string, details?: Record<string, unknown>): void {
const sanitizedJid = this.sanitizeJid(jid)
this.log(
'info',
@@ -398,7 +398,7 @@ export class BaileysLogger implements ILogger {
messageType,
jid: sanitizedJid,
...details,
category: 'message',
category: 'message'
},
`Message ${direction}: ${messageType}`
)
@@ -407,12 +407,7 @@ export class BaileysLogger implements ILogger {
/**
* Log media-specific event
*/
logMedia(
operation: 'upload' | 'download',
mediaType: string,
size: number,
details?: Record<string, unknown>
): void {
logMedia(operation: 'upload' | 'download', mediaType: string, size: number, details?: Record<string, unknown>): void {
this.log(
'info',
{
@@ -421,7 +416,7 @@ export class BaileysLogger implements ILogger {
sizeBytes: size,
sizeFormatted: this.formatBytes(size),
...details,
category: 'media',
category: 'media'
},
`Media ${operation}: ${mediaType}`
)
@@ -440,6 +435,7 @@ export class BaileysLogger implements ILogger {
return `${localPart.substring(0, 4)}****@${domainPart}`
}
}
return jid
}
@@ -499,9 +495,10 @@ let defaultBaileysLogger: BaileysLogger | null = null
export function getDefaultBaileysLogger(): BaileysLogger {
if (!defaultBaileysLogger) {
defaultBaileysLogger = createBaileysLogger({
level: 'info',
level: 'info'
})
}
return defaultBaileysLogger
}
@@ -557,6 +554,7 @@ function safeStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): s
const items = value.map(v => safeStringify(v, seen))
return `[${items.join(', ')}]`
}
return `[Array(${value.length})]`
}
@@ -571,6 +569,7 @@ function safeStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): s
})
return `{${pairs.join(', ')}}`
}
return `{Object(${keys.length} keys)}`
} catch {
return '[Object]'
@@ -584,7 +583,7 @@ function safeStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): s
* Format data object for single-line or multi-line output
* Handles circular references, Error objects, arrays, and undefined values
*/
function formatLogData(data: Record<string, unknown>, singleLine: boolean = true): string {
function formatLogData(data: Record<string, unknown>, singleLine = true): string {
if (!data || Object.keys(data).length === 0) return ''
const seen = new WeakSet<object>()
@@ -599,15 +598,21 @@ function formatLogData(data: Record<string, unknown>, singleLine: boolean = true
// Multi-line format - use safe replacer for JSON.stringify
try {
return JSON.stringify(data, (key, value) => {
if (value instanceof Error) {
return { name: value.name, message: value.message, stack: value.stack }
}
if (typeof value === 'bigint') {
return `${value}n`
}
return value
}, 2)
return JSON.stringify(
data,
(key, value) => {
if (value instanceof Error) {
return { name: value.name, message: value.message, stack: value.stack }
}
if (typeof value === 'bigint') {
return `${value}n`
}
return value
},
2
)
} catch {
// Fallback for circular references or other issues
return safeStringify(data, seen)
@@ -635,11 +640,7 @@ export type EventBufferLogType =
* logEventBuffer('buffer_flush', { flushCount: 10, historyCacheSize: 5, mode: 'aggressive' })
* // Output: [BAILEYS] 🔄 Event buffer flushed { flushCount: 10, historyCacheSize: 5, mode: 'aggressive' }
*/
export function logEventBuffer(
type: EventBufferLogType,
data?: Record<string, unknown>,
sessionName?: string
): void {
export function logEventBuffer(type: EventBufferLogType, data?: Record<string, unknown>, sessionName?: string): void {
if (!isBaileysLogEnabled()) return
const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]'
@@ -711,6 +712,7 @@ export function logBufferMetrics(
console.log(`${prefix} isHealthy: ${metrics.adaptive.isHealthy}`)
console.log(`${prefix} }`)
}
console.log(`${prefix} }`)
}
@@ -721,11 +723,7 @@ export function logBufferMetrics(
* logMessageSent('3EB02FA562D6CCC0876CDE', '5511999999999@s.whatsapp.net')
* // Output: [BAILEYS] 📤 Message sent: 3EB02FA562D6CCC0876CDE → 5511999999999@s.whatsapp.net
*/
export function logMessageSent(
messageId: string,
recipientJid: string,
sessionName?: string
): void {
export function logMessageSent(messageId: string, recipientJid: string, sessionName?: string): void {
if (!isBaileysLogEnabled()) return
const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]'
@@ -739,11 +737,7 @@ export function logMessageSent(
* logMessageReceived('A5E0349897A3F16F3F2778EEF94A065F', '238315571802285@lid')
* // Output: [BAILEYS] 📥 Message received: A5E0349897A3F16F3F2778EEF94A065F ← 238315571802285@lid
*/
export function logMessageReceived(
messageId: string,
senderJid: string,
sessionName?: string
): void {
export function logMessageReceived(messageId: string, senderJid: string, sessionName?: string): void {
if (!isBaileysLogEnabled()) return
const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]'
+18 -16
View File
@@ -1,5 +1,5 @@
import { existsSync, readFileSync } from 'fs'
import { platform, release } from 'os'
import { readFileSync, existsSync } from 'fs'
import { proto } from '../../WAProto/index.js'
import type { BrowsersMap } from '../Types'
@@ -32,7 +32,7 @@ const FALLBACK_VERSIONS = {
ubuntu: '24.04.1',
macOS: '15.2',
windows: '10.0.26100',
baileys: '6.5.0',
baileys: '6.5.0'
} as const
/**
@@ -49,7 +49,7 @@ const DARWIN_TO_MACOS: Readonly<Record<number, number>> = {
22: 13, // Ventura
21: 12, // Monterey
20: 11, // Big Sur
19: 10, // Catalina (10.15)
19: 10 // Catalina (10.15)
} as const
/**
@@ -67,7 +67,7 @@ const PLATFORM_MAP: Readonly<Record<NodeJS.Platform, string | undefined>> = {
netbsd: undefined,
openbsd: 'OpenBSD',
sunos: 'Solaris',
win32: 'Windows',
win32: 'Windows'
} as const
// ============================================================
@@ -93,6 +93,7 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
entries.push([key.toUpperCase(), value.toString()])
}
}
return new Map(entries)
})()
@@ -231,7 +232,7 @@ const detectOSVersions = (): Readonly<{
ubuntu: currentPlatform === 'linux' ? getLinuxVersion() : FALLBACK_VERSIONS.ubuntu,
macOS: currentPlatform === 'darwin' ? getMacOSVersion() : FALLBACK_VERSIONS.macOS,
windows: currentPlatform === 'win32' ? getWindowsVersion() : FALLBACK_VERSIONS.windows,
baileys: FALLBACK_VERSIONS.baileys,
baileys: FALLBACK_VERSIONS.baileys
}
}
@@ -270,6 +271,7 @@ const normalizeBrowserKey = (browser: unknown): string | null => {
if (typeof browser !== 'string') {
return null
}
const normalized = browser.trim()
return normalized.length > 0 ? normalized.toUpperCase() : null
}
@@ -285,16 +287,16 @@ const getAppropriateVersion = (): string => {
const currentPlatform = platform()
switch (currentPlatform) {
case 'darwin':
return OS_VERSIONS.macOS
case 'win32':
return OS_VERSIONS.windows
case 'linux':
return OS_VERSIONS.ubuntu
default:
// For other platforms, try os.release() directly
const version = release()
return version || DEFAULT_OS_VERSION
case 'darwin':
return OS_VERSIONS.macOS
case 'win32':
return OS_VERSIONS.windows
case 'linux':
return OS_VERSIONS.ubuntu
default:
// For other platforms, try os.release() directly
const version = release()
return version || DEFAULT_OS_VERSION
}
} catch {
return DEFAULT_OS_VERSION
@@ -329,7 +331,7 @@ export const Browsers: BrowsersMap = {
macOS: (browser: string): [string, string, string] => ['Mac OS', browser, OS_VERSIONS.macOS],
windows: (browser: string): [string, string, string] => ['Windows', browser, OS_VERSIONS.windows],
baileys: (browser: string): [string, string, string] => ['Baileys', browser, OS_VERSIONS.baileys],
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()],
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()]
} as const
/**
+15 -13
View File
@@ -88,7 +88,7 @@ export class Cache<V = unknown> {
namespace: options.namespace ?? 'default',
onExpire: options.onExpire ?? (() => {}),
collectMetrics: options.collectMetrics ?? true,
metricName: options.metricName ?? 'cache',
metricName: options.metricName ?? 'cache'
}
this.namespace = this.options.namespace
@@ -98,10 +98,10 @@ export class Cache<V = unknown> {
max: this.options.maxSize,
ttl: this.options.ttl,
updateAgeOnGet: this.options.updateAgeOnGet,
sizeCalculation: (item) => this.options.sizeCalculation(item.value),
sizeCalculation: item => this.options.sizeCalculation(item.value),
dispose: (value, key) => {
this.options.onExpire(key, value.value)
},
}
})
}
@@ -151,7 +151,7 @@ export class Cache<V = unknown> {
return {
value: item.value,
hit: true,
key,
key
}
}
@@ -163,7 +163,7 @@ export class Cache<V = unknown> {
return {
value: undefined,
hit: false,
key,
key
}
}
@@ -180,7 +180,7 @@ export class Cache<V = unknown> {
createdAt: now,
expiresAt: now + itemTtl,
accessCount: 0,
lastAccess: now,
lastAccess: now
}
this.cache.set(fullKey, item, { ttl: itemTtl })
@@ -291,7 +291,7 @@ export class Cache<V = unknown> {
misses: this.stats.misses,
size: this.cache.size,
maxSize: this.options.maxSize,
hitRate: total > 0 ? this.stats.hits / total : 0,
hitRate: total > 0 ? this.stats.hits / total : 0
}
}
@@ -307,14 +307,14 @@ export class Cache<V = unknown> {
*/
keys(): string[] {
const prefix = `${this.namespace}:`
return Array.from(this.cache.keys()).map((k) => (k.startsWith(prefix) ? k.slice(prefix.length) : k))
return Array.from(this.cache.keys()).map(k => (k.startsWith(prefix) ? k.slice(prefix.length) : k))
}
/**
* Return all values
*/
values(): V[] {
return Array.from(this.cache.values()).map((item) => item.value)
return Array.from(this.cache.values()).map(item => item.value)
}
/**
@@ -324,7 +324,7 @@ export class Cache<V = unknown> {
const prefix = `${this.namespace}:`
return Array.from(this.cache.entries()).map(([key, item]) => ({
key: key.startsWith(prefix) ? key.slice(prefix.length) : key,
item,
item
}))
}
@@ -468,7 +468,7 @@ export function cached<V>(options: CacheOptions<V> & { keyGenerator?: (...args:
/**
* Wrapper for function with cache
*/
export function withCache<T extends (...args: unknown[]) => unknown>(
export function withCache<T extends(...args: unknown[]) => unknown>(
fn: T,
options: CacheOptions<ReturnType<T>> & { keyGenerator?: (...args: Parameters<T>) => string } = {}
): T {
@@ -486,7 +486,7 @@ export function withCache<T extends (...args: unknown[]) => unknown>(
const result = fn(...args) as ReturnType<T>
if (result instanceof Promise) {
return result.then((value) => {
return result.then(value => {
cache.set(key, value as ReturnType<T>)
return value
}) as ReturnType<T>
@@ -505,8 +505,9 @@ const globalCaches: Map<string, Cache<any>> = new Map()
export function getGlobalCache<V>(namespace: string, options?: CacheOptions<V>): Cache<V> {
if (!globalCaches.has(namespace)) {
globalCaches.set(namespace, new Cache<V>({ ...options, namespace }) as Cache<V>)
globalCaches.set(namespace, new Cache<V>({ ...options, namespace }))
}
return globalCaches.get(namespace) as Cache<V>
}
@@ -514,6 +515,7 @@ export function clearGlobalCaches(): void {
for (const cache of globalCaches.values()) {
cache.clear()
}
globalCaches.clear()
}
+2 -8
View File
@@ -19,13 +19,13 @@ import {
type MessageLabelAssociation
} from '../Types/LabelAssociation'
import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
import { expandAppStateKeys } from './wasm-bridge'
import { aesDecrypt, aesEncrypt, hmacSign } from './crypto'
import { toNumber } from './generics'
import type { ILogger } from './logger'
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
import { downloadContentFromMessage } from './messages-media'
import { emitSyncActionResults, processContactAction } from './sync-action-utils'
import { expandAppStateKeys } from './wasm-bridge'
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSyncKeyData | null | undefined>
@@ -350,13 +350,7 @@ export const decodeSyncdPatch = async (
throw new Boom('Missing patchMac in patch message', { statusCode: 500 })
}
const patchMac = generatePatchMac(
msgSnapshotMac,
mutationmacs,
toNumber(msgVersion),
name,
mainKey.patchMacKey
)
const patchMac = generatePatchMac(msgSnapshotMac, mutationmacs, toNumber(msgVersion), name, mainKey.patchMacKey)
if (Buffer.compare(patchMac, msgPatchMac) !== 0) {
throw new Boom('Invalid patch mac')
}
+29 -46
View File
@@ -162,15 +162,17 @@ export class CircuitBreaker extends EventEmitter {
volumeThreshold: options.volumeThreshold ?? 5,
shouldCountError: options.shouldCountError ?? (() => true),
collectMetrics: options.collectMetrics ?? true,
fallback: options.fallback ?? (() => {
throw new CircuitOpenError(this.options.name, this.state)
}),
fallback:
options.fallback ??
(() => {
throw new CircuitOpenError(this.options.name, this.state)
}),
onStateChange: options.onStateChange ?? (() => {}),
onFailure: options.onFailure ?? (() => {}),
onSuccess: options.onSuccess ?? (() => {}),
onOpen: options.onOpen ?? (() => {}),
onClose: options.onClose ?? (() => {}),
onHalfOpen: options.onHalfOpen ?? (() => {}),
onHalfOpen: options.onHalfOpen ?? (() => {})
}
this.lastStateChange = Date.now()
@@ -256,11 +258,11 @@ export class CircuitBreaker extends EventEmitter {
}, this.options.timeout)
Promise.resolve(operation())
.then((result) => {
.then(result => {
clearTimeout(timer)
resolve(result)
})
.catch((error) => {
.catch(error => {
clearTimeout(timer)
reject(error)
})
@@ -327,10 +329,7 @@ export class CircuitBreaker extends EventEmitter {
// Check if we should trip the circuit
const recentFailures = this.failureRecords.length
if (
this.totalCalls >= this.options.volumeThreshold &&
recentFailures >= this.options.failureThreshold
) {
if (this.totalCalls >= this.options.volumeThreshold && recentFailures >= this.options.failureThreshold) {
this.transitionTo('open')
}
}
@@ -341,9 +340,7 @@ export class CircuitBreaker extends EventEmitter {
*/
private cleanOldFailures(): void {
const cutoff = Date.now() - this.options.failureWindow
this.failureRecords = this.failureRecords.filter(
(record) => record.timestamp > cutoff
)
this.failureRecords = this.failureRecords.filter(record => record.timestamp > cutoff)
}
/**
@@ -451,9 +448,7 @@ export class CircuitBreaker extends EventEmitter {
getStats(): CircuitBreakerStats {
this.cleanOldFailures()
const failureRate = this.totalCalls > 0
? (this.totalFailures / this.totalCalls) * 100
: 0
const failureRate = this.totalCalls > 0 ? (this.totalFailures / this.totalCalls) * 100 : 0
return {
state: this.state,
@@ -471,7 +466,7 @@ export class CircuitBreaker extends EventEmitter {
lastStateChange: this.lastStateChange,
isOpen: this.isOpen(),
isClosed: this.isClosed(),
isHalfOpen: this.isHalfOpen(),
isHalfOpen: this.isHalfOpen()
}
}
@@ -489,6 +484,7 @@ export class CircuitBreaker extends EventEmitter {
if (this.resetTimer) {
clearTimeout(this.resetTimer)
}
this.failureRecords = []
this.removeAllListeners()
}
@@ -515,6 +511,7 @@ export class CircuitBreakerRegistry {
const breaker = new CircuitBreaker({ ...options, name })
this.breakers.set(name, breaker)
}
return this.breakers.get(name)!
}
@@ -534,6 +531,7 @@ export class CircuitBreakerRegistry {
breaker.destroy()
return this.breakers.delete(name)
}
return false
}
@@ -552,6 +550,7 @@ export class CircuitBreakerRegistry {
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.getStats()
}
return stats
}
@@ -571,6 +570,7 @@ export class CircuitBreakerRegistry {
for (const breaker of this.breakers.values()) {
breaker.destroy()
}
this.breakers.clear()
}
}
@@ -624,7 +624,7 @@ export function circuitBreaker(options: Omit<CircuitBreakerOptions, 'name'> & {
* )
* ```
*/
export function withCircuitBreaker<T extends (...args: unknown[]) => unknown>(
export function withCircuitBreaker<T extends(...args: unknown[]) => unknown>(
fn: T,
options: CircuitBreakerOptions
): T {
@@ -655,7 +655,7 @@ export function getCircuitHealth(): {
return {
healthy: openCircuits.length === 0,
openCircuits,
stats,
stats
}
}
@@ -676,19 +676,8 @@ export function getCircuitHealth(): {
* }
* ```
*/
export function createPreKeyCircuitBreaker(
customOptions?: Partial<CircuitBreakerOptions>
): CircuitBreaker {
const preKeyErrorPatterns = [
'prekey',
'pre-key',
'session',
'signal',
'encrypt',
'decrypt',
'cipher',
'key',
]
export function createPreKeyCircuitBreaker(customOptions?: Partial<CircuitBreakerOptions>): CircuitBreaker {
const preKeyErrorPatterns = ['prekey', 'pre-key', 'session', 'signal', 'encrypt', 'decrypt', 'cipher', 'key']
return new CircuitBreaker({
name: 'prekey-operations',
@@ -698,18 +687,16 @@ export function createPreKeyCircuitBreaker(
successThreshold: 2,
shouldCountError: (error: Error) => {
const message = error.message.toLowerCase()
return preKeyErrorPatterns.some((pattern) => message.includes(pattern))
return preKeyErrorPatterns.some(pattern => message.includes(pattern))
},
...customOptions,
...customOptions
})
}
/**
* Create a circuit breaker for WebSocket connection operations
*/
export function createConnectionCircuitBreaker(
customOptions?: Partial<CircuitBreakerOptions>
): CircuitBreaker {
export function createConnectionCircuitBreaker(customOptions?: Partial<CircuitBreakerOptions>): CircuitBreaker {
const connectionErrorPatterns = [
'econnrefused',
'econnreset',
@@ -718,7 +705,7 @@ export function createConnectionCircuitBreaker(
'socket',
'websocket',
'connection',
'network',
'network'
]
return new CircuitBreaker({
@@ -730,20 +717,16 @@ export function createConnectionCircuitBreaker(
shouldCountError: (error: Error) => {
const message = error.message.toLowerCase()
const code = (error as NodeJS.ErrnoException).code?.toLowerCase() || ''
return connectionErrorPatterns.some(
(pattern) => message.includes(pattern) || code.includes(pattern)
)
return connectionErrorPatterns.some(pattern => message.includes(pattern) || code.includes(pattern))
},
...customOptions,
...customOptions
})
}
/**
* Create a circuit breaker for message sending operations
*/
export function createMessageCircuitBreaker(
customOptions?: Partial<CircuitBreakerOptions>
): CircuitBreaker {
export function createMessageCircuitBreaker(customOptions?: Partial<CircuitBreakerOptions>): CircuitBreaker {
return new CircuitBreaker({
name: 'message-operations',
failureThreshold: 5,
@@ -751,7 +734,7 @@ export function createMessageCircuitBreaker(
resetTimeout: 15000,
successThreshold: 2,
timeout: 30000,
...customOptions,
...customOptions
})
}
+19 -27
View File
@@ -19,7 +19,7 @@ import {
} from '../WABinary'
import { unpadRandomMax16 } from './generics'
import type { ILogger } from './logger'
import { retry, type RetryOptions, RetryExhaustedError } from './retry-utils'
import { retry, RetryExhaustedError, type RetryOptions } from './retry-utils'
export const getDecryptionJid = async (sender: string, repository: SignalRepositoryWithLIDStore): Promise<string> => {
if (isLidUser(sender) || isHostedLidUser(sender)) {
@@ -269,7 +269,7 @@ export const decryptMessageNode = (
meLid: string,
repository: SignalRepositoryWithLIDStore,
logger: ILogger,
autoCleanCorrupted: boolean = true
autoCleanCorrupted = true
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
return {
@@ -322,11 +322,12 @@ export const decryptMessageNode = (
switch (e2eType) {
case 'skmsg':
msgBuffer = await retry(
() => repository.decryptGroupMessage({
group: sender,
authorJid: author,
msg: content
}),
() =>
repository.decryptGroupMessage({
group: sender,
authorJid: author,
msg: content
}),
{
...DECRYPTION_RETRY_OPTIONS,
onRetry: (error, attempt, delay) => {
@@ -341,11 +342,12 @@ export const decryptMessageNode = (
case 'pkmsg':
case 'msg':
msgBuffer = await retry(
() => repository.decryptMessage({
jid: decryptionJid,
type: e2eType,
ciphertext: content
}),
() =>
repository.decryptMessage({
jid: decryptionJid,
type: e2eType,
ciphertext: content
}),
{
...DECRYPTION_RETRY_OPTIONS,
onRetry: (error, attempt, delay) => {
@@ -416,10 +418,7 @@ export const decryptMessageNode = (
)
} else {
// First occurrence - log as warning since auto-recovery will attempt
logger.warn(
errorContext,
'⚠️ Corrupted session detected - attempting auto-recovery'
)
logger.warn(errorContext, '⚠️ Corrupted session detected - attempting auto-recovery')
}
// Automatic cleanup of corrupted session (if enabled)
@@ -429,9 +428,8 @@ export const decryptMessageNode = (
// Mask only user portion of JID for privacy (preserve domain info)
const { user, server } = jidDecode(decryptionJid) || {}
const maskedUser = user && user.length > 8
? `${user.substring(0, 4)}****${user.substring(user.length - 4)}`
: user
const maskedUser =
user && user.length > 8 ? `${user.substring(0, 4)}****${user.substring(user.length - 4)}` : user
const maskedJid = maskedUser && server ? `${maskedUser}@${server}` : decryptionJid
logger.info(
@@ -439,19 +437,13 @@ export const decryptMessageNode = (
`🔄 Session Reset | JID: ${maskedJid} | Targeted: ${deletedCount} devices | Will recreate on next message`
)
} catch (cleanupErr) {
logger.error(
{ decryptionJid, err: cleanupErr },
'❌ Failed to cleanup corrupted session'
)
logger.error({ decryptionJid, err: cleanupErr }, '❌ Failed to cleanup corrupted session')
}
}
} else if (isSessionRecord) {
// Session record errors are transient - retry should handle them
if (isRetryExhausted) {
logger.error(
errorContext,
`Failed to decrypt: No session record found after ${err.attempts} attempts`
)
logger.error(errorContext, `Failed to decrypt: No session record found after ${err.attempts} attempts`)
} else {
logger.debug(errorContext, 'No session record - will retry')
}
+59 -38
View File
@@ -11,11 +11,11 @@ import type {
WAMessageKey
} from '../Types'
import { WAMessageStatus } from '../Types'
import { logEventBuffer } from './baileys-logger'
import { trimUndefined } from './generics'
import type { ILogger } from './logger'
import { updateMessageWithReaction, updateMessageWithReceipt } from './messages'
import { isRealMessage, shouldIncrementChatUnread } from './process-message'
import { logEventBuffer } from './baileys-logger'
// ============================================================================
// BUFFER CONFIGURATION - Environment Variable Support
@@ -329,6 +329,7 @@ class AdaptiveTimeoutCalculator {
} else if (this.currentTimeout >= this.maxTimeout * 0.8) {
return 'conservative'
}
return 'balanced'
}
@@ -339,6 +340,7 @@ class AdaptiveTimeoutCalculator {
if (this.eventTimestamps.length < 2) {
return 0
}
const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return 0
@@ -428,18 +430,20 @@ export const makeEventBuffer = (
const MAX_METRICS_QUEUE_SIZE = 1000 // Cap to prevent unbounded growth
if (config.enableMetrics) {
import('./prometheus-metrics').then(m => {
metricsModule = m
logger.debug({ queuedCount: metricsQueue.length }, '📊 Prometheus metrics loaded, flushing buffered metrics')
// Flush buffered metrics
metricsQueue.forEach(fn => fn())
metricsQueue = []
}).catch(() => {
logger.debug('Prometheus metrics not available for event buffer')
metricsImportFailed = true
// Clear queue to prevent memory leak
metricsQueue = []
})
import('./prometheus-metrics')
.then(m => {
metricsModule = m
logger.debug({ queuedCount: metricsQueue.length }, '📊 Prometheus metrics loaded, flushing buffered metrics')
// Flush buffered metrics
metricsQueue.forEach(fn => fn())
metricsQueue = []
})
.catch(() => {
logger.debug('Prometheus metrics not available for event buffer')
metricsImportFailed = true
// Clear queue to prevent memory leak
metricsQueue = []
})
}
// Helper to record metrics with buffer support
@@ -467,6 +471,7 @@ export const makeEventBuffer = (
clearTimeout(bufferTimeout)
bufferTimeout = null
}
if (flushPendingTimeout) {
clearTimeout(flushPendingTimeout)
flushPendingTimeout = null
@@ -486,10 +491,13 @@ export const makeEventBuffer = (
function checkBufferOverflow(): boolean {
if (currentEventCount >= config.maxBufferSize) {
stats.overflowsDetected++
logger.warn({
currentSize: currentEventCount,
maxSize: config.maxBufferSize
}, 'Buffer overflow detected, forcing flush')
logger.warn(
{
currentSize: currentEventCount,
maxSize: config.maxBufferSize
},
'Buffer overflow detected, forcing flush'
)
logEventBuffer('buffer_overflow', {
currentSize: currentEventCount,
maxSize: config.maxBufferSize
@@ -500,6 +508,7 @@ export const makeEventBuffer = (
} else if (!metricsImportFailed && metricsQueue.length < MAX_METRICS_QUEUE_SIZE) {
metricsQueue.push(() => metricsModule?.recordBufferOverflow())
}
flush(true)
return true
}
@@ -507,11 +516,14 @@ export const makeEventBuffer = (
// Warn if approaching threshold
const threshold = config.maxBufferSize * config.bufferWarnThreshold
if (currentEventCount >= threshold && currentEventCount < config.maxBufferSize) {
logger.debug({
currentSize: currentEventCount,
threshold,
maxSize: config.maxBufferSize
}, 'Buffer approaching overflow threshold')
logger.debug(
{
currentSize: currentEventCount,
threshold,
maxSize: config.maxBufferSize
},
'Buffer approaching overflow threshold'
)
}
return false
@@ -524,11 +536,14 @@ export const makeEventBuffer = (
if (historyCache.size > config.maxHistoryCacheSize) {
const removed = historyCache.cleanup(config.lruCleanupRatio)
stats.lruCleanups++
logger.debug({
removed: removed.length,
remaining: historyCache.size,
maxSize: config.maxHistoryCacheSize
}, 'LRU cleanup performed on history cache')
logger.debug(
{
removed: removed.length,
remaining: historyCache.size,
maxSize: config.maxHistoryCacheSize
},
'LRU cleanup performed on history cache'
)
logEventBuffer('cache_cleanup', {
removed: removed.length,
remaining: historyCache.size
@@ -558,9 +573,7 @@ export const makeEventBuffer = (
clearAllTimers()
// Use adaptive timeout if enabled
const timeout = config.enableAdaptiveTimeout
? adaptiveTimeout.getTimeout()
: config.bufferTimeoutMs
const timeout = config.enableAdaptiveTimeout ? adaptiveTimeout.getTimeout() : config.bufferTimeoutMs
stats.currentTimeout = timeout
bufferTimeout = setTimeout(() => {
@@ -577,7 +590,7 @@ export const makeEventBuffer = (
bufferCount++
}
function flush(force: boolean = false): boolean {
function flush(force = false): boolean {
if (destroyed) {
logger.warn('Attempted to flush destroyed event buffer')
return false
@@ -598,9 +611,10 @@ export const makeEventBuffer = (
stats.lastFlushAt = Date.now()
// Update average events per flush
stats.avgEventsPerFlush = stats.totalFlushes > 0
? (stats.avgEventsPerFlush * (stats.totalFlushes - 1) + eventCount) / stats.totalFlushes
: eventCount
stats.avgEventsPerFlush =
stats.totalFlushes > 0
? (stats.avgEventsPerFlush * (stats.totalFlushes - 1) + eventCount) / stats.totalFlushes
: eventCount
// Clear timeouts
clearAllTimers()
@@ -617,7 +631,9 @@ export const makeEventBuffer = (
if (metricsModule) {
metricsModule.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy())
} else if (!metricsImportFailed && metricsQueue.length < MAX_METRICS_QUEUE_SIZE) {
metricsQueue.push(() => metricsModule?.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy()))
metricsQueue.push(() =>
metricsModule?.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy())
)
}
}
@@ -790,9 +806,7 @@ export const makeEventBuffer = (
...stats,
currentBufferSize: currentEventCount,
historyCacheSize: historyCache.size,
currentTimeout: config.enableAdaptiveTimeout
? adaptiveTimeout.getTimeout()
: config.bufferTimeoutMs
currentTimeout: config.enableAdaptiveTimeout ? adaptiveTimeout.getTimeout() : config.bufferTimeoutMs
}
},
getConfig() {
@@ -817,6 +831,7 @@ export const makeEventBuffer = (
if (functionTimeout) {
clearTimeout(functionTimeout)
}
functionTimeout = setTimeout(() => {
functionTimeout = null
if (isBuffering && bufferCount === 1 && !destroyed) {
@@ -832,6 +847,7 @@ export const makeEventBuffer = (
clearTimeout(functionTimeout)
functionTimeout = null
}
throw error
} finally {
bufferCount = Math.max(0, bufferCount - 1)
@@ -853,6 +869,7 @@ export const makeEventBuffer = (
if (destroyed) {
throw new Error('Cannot add listener to destroyed event buffer')
}
return ev.on(...args)
},
off: (...args) => ev.off(...args),
@@ -987,6 +1004,7 @@ function append<E extends BufferableEvent>(
logger.debug({ update }, 'chats.update: update missing id, skipping')
continue
}
const conditionMatches = update.conditional ? update.conditional(data) : true
if (conditionMatches) {
delete update.conditional
@@ -1068,6 +1086,7 @@ function append<E extends BufferableEvent>(
logger.debug({ update }, 'contacts.update: update missing id, skipping')
continue
}
// merge into prior upsert
const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
if (upsert) {
@@ -1193,6 +1212,7 @@ function append<E extends BufferableEvent>(
logger.debug({ update }, 'groups.update: update missing id, skipping')
continue
}
const groupUpdate = data.groupUpdates[id] || {}
if (!data.groupUpdates[id]) {
data.groupUpdates[id] = Object.assign(groupUpdate, update)
@@ -1239,6 +1259,7 @@ function append<E extends BufferableEvent>(
logger.debug({ messageKey: message.key }, 'decrementChatReadCounter: remoteJid missing, skipping')
return
}
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
if (
isRealMessage(message) &&
+5 -1
View File
@@ -253,7 +253,11 @@ export const fetchLatestBaileysVersion = async (options: RequestInit = {}) => {
const versionMatch = versionLine.match(/const version = \[(\d+),\s*(\d+),\s*(\d+)\]/)
if (versionMatch) {
const version = [parseInt(versionMatch[1] ?? '0'), parseInt(versionMatch[2] ?? '0'), parseInt(versionMatch[3] ?? '0')] as WAVersion
const version = [
parseInt(versionMatch[1] ?? '0'),
parseInt(versionMatch[2] ?? '0'),
parseInt(versionMatch[3] ?? '0')
] as WAVersion
return {
version,
+1 -1
View File
@@ -6,8 +6,8 @@
* @module Utils/health-status
*/
import { getVersionCacheStatus } from './version-cache.js'
import { globalCircuitRegistry } from './circuit-breaker.js'
import { getVersionCacheStatus } from './version-cache.js'
/**
* Circuit breaker health information
+8 -14
View File
@@ -3,10 +3,6 @@ import { inflate } from 'zlib'
import { proto } from '../../WAProto/index.js'
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types'
import { WAMessageStubType } from '../Types'
import { toNumber } from './generics'
import { normalizeMessageContent } from './messages'
import { downloadContentFromMessage } from './messages-media'
import type { ILogger } from './logger.js'
import {
isAnyLidUser,
isAnyPnUser,
@@ -16,6 +12,10 @@ import {
jidDecode,
jidNormalizedUser
} from '../WABinary/index.js'
import { toNumber } from './generics'
import type { ILogger } from './logger.js'
import { normalizeMessageContent } from './messages'
import { downloadContentFromMessage } from './messages-media'
const inflatePromise = promisify(inflate)
@@ -255,10 +255,7 @@ const extractPnFromMessages = (messages: proto.IHistorySyncMsg[]): string | unde
* @see https://github.com/WhiskeySockets/Baileys/issues/2263
*/
export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger) => {
logger?.trace(
{ syncType: item.syncType, progress: item.progress },
'processing history sync'
)
logger?.trace({ syncType: item.syncType, progress: item.progress }, 'processing history sync')
const messages: WAMessage[] = []
const contacts: Contact[] = []
const chats: Chat[] = []
@@ -275,6 +272,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
if (!mapping.lid || !mapping.pn) {
return
}
lidPnMap.set(mapping.lid, mapping)
}
@@ -298,11 +296,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
// Source 2: Extract LID-PN mapping from conversation object
// This handles cases where the mapping isn't in phoneNumberToLidMappings
const conversationMapping = extractLidPnFromConversation(
chatId,
chat.lidJid,
chat.pnJid
)
const conversationMapping = extractLidPnFromConversation(chatId, chat.lidJid, chat.pnJid)
if (conversationMapping) {
addLidPnMapping(conversationMapping)
} else if (isAnyLidUser(chatId) && !chat.pnJid) {
@@ -370,7 +364,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
break
case proto.HistorySync.HistorySyncType.PUSH_NAME:
for (const c of (item.pushnames ?? [])) {
for (const c of item.pushnames ?? []) {
contacts.push({ id: c.id!, notify: c.pushname! })
}
+2 -8
View File
@@ -126,10 +126,7 @@ export async function handleIdentityChange(
// Skip companion devices - they don't need session refresh
const decoded = jidDecode(from)
if (decoded?.device && decoded.device !== 0) {
ctx.logger.debug(
{ jid: from, device: decoded.device },
'ignoring identity change from companion device'
)
ctx.logger.debug({ jid: from, device: decoded.device }, 'ignoring identity change from companion device')
return { action: 'skipped_companion_device', device: decoded.device }
}
@@ -178,10 +175,7 @@ export async function handleIdentityChange(
await ctx.assertSessions([from], true)
return { action: 'session_refreshed', hadExistingSession: hasExistingSession.exists }
} catch (error) {
ctx.logger.warn(
{ error, jid: from },
'failed to assert sessions after identity change'
)
ctx.logger.warn({ error, jid: from }, 'failed to assert sessions after identity change')
return { action: 'session_refresh_failed', error }
}
}
+13 -17
View File
@@ -9,9 +9,9 @@
* - Compatibilidade com Pino, Console e StructuredLogger
*/
import type { ILogger } from './logger.js'
import type P from 'pino'
import { StructuredLogger, createStructuredLogger, type LogLevel, LOG_LEVEL_VALUES } from './structured-logger.js'
import type { ILogger } from './logger.js'
import { createStructuredLogger, LOG_LEVEL_VALUES, type LogLevel, StructuredLogger } from './structured-logger.js'
/**
* Tipo de logger suportado
@@ -43,7 +43,7 @@ const PINO_LEVEL_MAPPING: Record<number, LogLevel> = {
30: 'info',
40: 'warn',
50: 'error',
60: 'fatal',
60: 'fatal'
}
/**
@@ -56,7 +56,7 @@ const STRUCTURED_TO_PINO_LEVEL: Record<LogLevel, number> = {
warn: 40,
error: 50,
fatal: 60,
silent: 100,
silent: 100
}
/**
@@ -74,7 +74,7 @@ export class LoggerAdapter implements ILogger {
targetType: config.targetType || 'structured',
levelMapping: config.levelMapping,
contextTransformer: config.contextTransformer,
logFilter: config.logFilter,
logFilter: config.logFilter
}
}
@@ -133,6 +133,7 @@ export class LoggerAdapter implements ILogger {
if (this.config.logFilter) {
return this.config.logFilter(level, msg, obj)
}
return true
}
@@ -191,7 +192,7 @@ export class PinoToStructuredAdapter implements ILogger {
this.pinoLogger = pinoLogger
this.structuredLogger = createStructuredLogger({
level: this.mapPinoLevel(pinoLogger.level),
...structuredLoggerConfig,
...structuredLoggerConfig
})
}
@@ -212,7 +213,7 @@ export class PinoToStructuredAdapter implements ILogger {
warn: 'warn',
error: 'error',
fatal: 'fatal',
silent: 'silent',
silent: 'silent'
}
return levelMap[pinoLevel] || 'info'
}
@@ -258,10 +259,7 @@ export class PinoToStructuredAdapter implements ILogger {
/**
* Factory para criar adapter baseado no tipo de logger
*/
export function createLoggerAdapter(
logger: ILogger,
config?: Partial<LoggerAdapterConfig>
): LoggerAdapter {
export function createLoggerAdapter(logger: ILogger, config?: Partial<LoggerAdapterConfig>): LoggerAdapter {
return new LoggerAdapter(logger, config)
}
@@ -283,13 +281,14 @@ export function normalizeLogger(logger: unknown): ILogger {
if (typeof logObj.child === 'function') {
return normalizeLogger((logObj.child as (obj: Record<string, unknown>) => unknown)(obj))
}
return normalizeLogger(logger)
},
trace: createLogMethod(logObj, 'trace'),
debug: createLogMethod(logObj, 'debug'),
info: createLogMethod(logObj, 'info'),
warn: createLogMethod(logObj, 'warn'),
error: createLogMethod(logObj, 'error'),
error: createLogMethod(logObj, 'error')
}
}
@@ -320,10 +319,7 @@ export function isILogger(obj: unknown): obj is ILogger {
/**
* Cria método de log genérico
*/
function createLogMethod(
logger: Record<string, unknown>,
level: string
): (obj: unknown, msg?: string) => void {
function createLogMethod(logger: Record<string, unknown>, level: string): (obj: unknown, msg?: string) => void {
return (obj: unknown, msg?: string) => {
if (typeof logger[level] === 'function') {
;(logger[level] as (obj: unknown, msg?: string) => void)(obj, msg)
@@ -370,7 +366,7 @@ export function createConsoleLogger(prefix?: string): ILogger {
},
error(obj: unknown, msg?: string): void {
console.error(formatMessage('error', obj, msg))
},
}
}
}
+8 -7
View File
@@ -55,8 +55,8 @@ function loadLoggerConfig(): LoggerConfig {
levelFilters: {
info: process.env.LOGGER_INFO !== 'false',
warn: process.env.LOGGER_WARN !== 'false',
error: process.env.LOGGER_ERROR !== 'false',
},
error: process.env.LOGGER_ERROR !== 'false'
}
}
}
@@ -89,7 +89,7 @@ function createFilteredLogger(baseLogger: PinoLogger, config: LoggerConfig): ILo
debug: baseLogger.debug.bind(baseLogger),
info: config.levelFilters.info ? baseLogger.info.bind(baseLogger) : noop,
warn: config.levelFilters.warn ? baseLogger.warn.bind(baseLogger) : noop,
error: config.levelFilters.error ? baseLogger.error.bind(baseLogger) : noop,
error: config.levelFilters.error ? baseLogger.error.bind(baseLogger) : noop
}
}
@@ -98,6 +98,7 @@ function createFilteredLogger(baseLogger: PinoLogger, config: LoggerConfig): ILo
*/
function createSilentLogger(): ILogger {
const noop = () => {}
const silentLogger: ILogger = {
level: 'silent',
child: () => silentLogger,
@@ -105,7 +106,7 @@ function createSilentLogger(): ILogger {
debug: noop,
info: noop,
warn: noop,
error: noop,
error: noop
}
return silentLogger
}
@@ -124,7 +125,7 @@ function createLogger(): ILogger {
// Create pino options
const pinoOptions: P.LoggerOptions = {
level: config.level,
timestamp: () => `,"time":"${new Date().toJSON()}"`,
timestamp: () => `,"time":"${new Date().toJSON()}"`
}
// Add pretty printing when explicitly set and available
@@ -134,8 +135,8 @@ function createLogger(): ILogger {
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
ignore: 'pid,hostname'
}
}
}
+4 -7
View File
@@ -46,7 +46,7 @@ export enum RetryReason {
/** ADV (Announcement Delivery Verification) failure */
AdvFailure = 12,
/** Status revoke was delayed */
StatusRevokeDelay = 13,
StatusRevokeDelay = 13
}
/**
@@ -56,7 +56,7 @@ export enum RetryReason {
*/
export const MAC_ERROR_CODES = new Set<RetryReason>([
RetryReason.SignalErrorInvalidMessage,
RetryReason.SignalErrorBadMac,
RetryReason.SignalErrorBadMac
])
/**
@@ -66,7 +66,7 @@ export const SESSION_ERROR_CODES = new Set<RetryReason>([
RetryReason.SignalErrorNoSession,
RetryReason.SignalErrorInvalidSession,
RetryReason.SignalErrorInvalidKey,
RetryReason.SignalErrorInvalidKeyId,
RetryReason.SignalErrorInvalidKeyId
])
export interface RecentMessageKey {
to: string
@@ -196,10 +196,7 @@ export class MessageRetryManager {
const reasonName = RetryReason[errorCode] || `code_${errorCode}`
metrics.signalMacErrors?.inc({ action: 'session_recreation' })
metrics.signalSessionRecreations?.inc({ reason: 'mac_error' })
this.logger.warn(
{ jid, errorCode: reasonName },
'MAC error detected, forcing immediate session recreation'
)
this.logger.warn({ jid, errorCode: reasonName }, 'MAC error detected, forcing immediate session recreation')
return {
reason: `MAC error (${reasonName}) - contact may have reinstalled WhatsApp`,
recreate: true
+10 -6
View File
@@ -127,13 +127,17 @@ const extractVideoThumb = async (
size: { width: number; height: number }
) =>
new Promise<void>((resolve, reject) => {
execFile('ffmpeg', ['-ss', time, '-i', path, '-y', '-vf', `scale=${size.width}:-1`, '-vframes', '1', '-f', 'image2', destPath], err => {
if (err) {
reject(err)
} else {
resolve()
execFile(
'ffmpeg',
['-ss', time, '-i', path, '-y', '-vf', `scale=${size.width}:-1`, '-vframes', '1', '-f', 'image2', destPath],
err => {
if (err) {
reject(err)
} else {
resolve()
}
}
})
)
})
export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | string, width = 32) => {
+91 -92
View File
@@ -47,8 +47,8 @@ import {
getRawMediaUploadData,
type MediaDownloadOptions
} from './messages-media'
import { prepareStickerPackMessage } from './sticker-pack.js'
import { shouldIncludeReportingToken } from './reporting-utils'
import { prepareStickerPackMessage } from './sticker-pack.js'
type ExtractByKey<T, K extends PropertyKey> = T extends Record<K, any> ? T : never
type RequireKey<T, K extends keyof T> = T & {
@@ -260,7 +260,10 @@ export const prepareWAMessageMedia = async (
})(),
(async () => {
try {
if ((requiresThumbnailComputation || requiresDurationComputation || requiresWaveformProcessing) && !originalFilePath) {
if (
(requiresThumbnailComputation || requiresDurationComputation || requiresWaveformProcessing) &&
!originalFilePath
) {
throw new Boom('Missing file path for processing')
}
@@ -513,7 +516,7 @@ export const generateButtonMessage = async (
// Determine header configuration
const hasMedia = !!(headerImage || headerVideo)
const header: proto.Message.InteractiveMessage.IHeader = {
title: hasMedia ? '' : (headerTitle || ''),
title: hasMedia ? '' : headerTitle || '',
subtitle: '',
hasMediaAttachment: hasMedia
}
@@ -635,34 +638,36 @@ export const generateCarouselMessage = async (
}
// Map cards to the carousel format (processing media)
const carouselCards = await Promise.all(cards.map(async (card) => {
const hasMedia = !!(card.image || card.video)
const carouselCards = await Promise.all(
cards.map(async card => {
const hasMedia = !!(card.image || card.video)
const header: any = {
title: card.title || '',
hasMediaAttachment: hasMedia
}
// Process media if present
if (hasMedia && mediaOptions) {
if (card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions)
header.imageMessage = imageMessage
} else if (card.video) {
const { videoMessage } = await prepareWAMessageMedia({ video: card.video }, mediaOptions)
header.videoMessage = videoMessage
const header: any = {
title: card.title || '',
hasMediaAttachment: hasMedia
}
}
return {
header,
body: { text: card.body || '' },
footer: card.footer ? { text: card.footer } : undefined,
nativeFlowMessage: {
buttons: card.buttons.map(formatNativeFlowButton)
// Process media if present
if (hasMedia && mediaOptions) {
if (card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions)
header.imageMessage = imageMessage
} else if (card.video) {
const { videoMessage } = await prepareWAMessageMedia({ video: card.video }, mediaOptions)
header.videoMessage = videoMessage
}
}
}
}))
return {
header,
body: { text: card.body || '' },
footer: card.footer ? { text: card.footer } : undefined,
nativeFlowMessage: {
buttons: card.buttons.map(formatNativeFlowButton)
}
}
})
)
// Build the interactive message with carousel
// Match Pastorini's EXACT working structure:
@@ -744,7 +749,7 @@ const LIST_LIMITS = {
MAX_ROW_TITLE: 24,
MAX_ROW_DESCRIPTION: 72,
MAX_ROW_ID: 200,
MAX_BUTTON_TEXT: 20,
MAX_BUTTON_TEXT: 20
} as const
/**
@@ -760,17 +765,13 @@ const validateListSections = (
}
if (sections.length > LIST_LIMITS.MAX_SECTIONS) {
throw new Boom(
`Maximum ${LIST_LIMITS.MAX_SECTIONS} sections allowed, got ${sections.length}`,
{ statusCode: 400 }
)
throw new Boom(`Maximum ${LIST_LIMITS.MAX_SECTIONS} sections allowed, got ${sections.length}`, { statusCode: 400 })
}
if (buttonText && buttonText.length > LIST_LIMITS.MAX_BUTTON_TEXT) {
throw new Boom(
`buttonText max ${LIST_LIMITS.MAX_BUTTON_TEXT} characters, got ${buttonText.length}`,
{ statusCode: 400 }
)
throw new Boom(`buttonText max ${LIST_LIMITS.MAX_BUTTON_TEXT} characters, got ${buttonText.length}`, {
statusCode: 400
})
}
let totalRows = 0
@@ -796,10 +797,7 @@ const validateListSections = (
for (const row of section.rows) {
const rowId = row.id || row.rowId || ''
if (rowId.length > LIST_LIMITS.MAX_ROW_ID) {
throw new Boom(
`Row ID max ${LIST_LIMITS.MAX_ROW_ID} characters, got ${rowId.length}`,
{ statusCode: 400 }
)
throw new Boom(`Row ID max ${LIST_LIMITS.MAX_ROW_ID} characters, got ${rowId.length}`, { statusCode: 400 })
}
if (row.title && row.title.length > LIST_LIMITS.MAX_ROW_TITLE) {
@@ -821,10 +819,7 @@ const validateListSections = (
}
if (totalRows > LIST_LIMITS.MAX_TOTAL_ROWS) {
throw new Boom(
`Maximum ${LIST_LIMITS.MAX_TOTAL_ROWS} total rows allowed, got ${totalRows}`,
{ statusCode: 400 }
)
throw new Boom(`Maximum ${LIST_LIMITS.MAX_TOTAL_ROWS} total rows allowed, got ${totalRows}`, { statusCode: 400 })
}
}
@@ -845,13 +840,15 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte
// Create native flow message with single_select button
const nativeFlowMessage = {
buttons: [{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: buttonText,
sections: formattedSections
})
}],
buttons: [
{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: buttonText,
sections: formattedSections
})
}
],
messageParamsJson: JSON.stringify({}),
messageVersion: 2
}
@@ -860,11 +857,13 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte
const interactiveMessage: proto.Message.IInteractiveMessage = {
body: { text: text || '' },
footer: footer ? { text: footer } : undefined,
header: title ? {
title,
subtitle: '',
hasMediaAttachment: false
} : undefined,
header: title
? {
title,
subtitle: '',
hasMediaAttachment: false
}
: undefined,
nativeFlowMessage
}
@@ -919,15 +918,7 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte
* ```
*/
export const generateProductListMessage = (options: ProductListMessageOptions): WAMessageContent => {
const {
title,
description,
buttonText,
footerText,
businessOwnerJid,
productSections,
headerImage
} = options
const { title, description, buttonText, footerText, businessOwnerJid, productSections, headerImage } = options
// Validation
if (!title || title.trim().length === 0) {
@@ -965,7 +956,9 @@ export const generateProductListMessage = (options: ProductListMessageOptions):
// Validate each product has a valid productId
for (const product of section.products) {
if (!product || typeof product.productId !== 'string' || product.productId.trim().length === 0) {
throw new Boom(`Each product in section "${section.title}" must have a non-empty productId`, { statusCode: 400 })
throw new Boom(`Each product in section "${section.title}" must have a non-empty productId`, {
statusCode: 400
})
}
}
}
@@ -992,9 +985,14 @@ export const generateProductListMessage = (options: ProductListMessageOptions):
// Add header image if provided (with validation)
if (headerImage) {
if (!headerImage.productId || typeof headerImage.productId !== 'string' || headerImage.productId.trim().length === 0) {
if (
!headerImage.productId ||
typeof headerImage.productId !== 'string' ||
headerImage.productId.trim().length === 0
) {
throw new Boom('headerImage.productId must be a non-empty string', { statusCode: 400 })
}
productListInfo.headerImage = {
productId: headerImage.productId,
jpegThumbnail: headerImage.jpegThumbnail
@@ -1037,9 +1035,7 @@ export const generateProductListMessage = (options: ProductListMessageOptions):
* await sock.sendMessage(jid, msg)
* ```
*/
export const generateProductCarouselMessage = (
options: ProductCarouselMessageOptions
): WAMessageContent => {
export const generateProductCarouselMessage = (options: ProductCarouselMessageOptions): WAMessageContent => {
const { businessOwnerJid, products, body } = options
if (!businessOwnerJid || typeof businessOwnerJid !== 'string' || businessOwnerJid.trim().length === 0) {
@@ -1069,7 +1065,7 @@ export const generateProductCarouselMessage = (
// Build cards array - each card is an IInteractiveMessage with collectionMessage
// collectionMessage references a product from the business catalog
const cards: proto.Message.IInteractiveMessage[] = products.map((product) => ({
const cards: proto.Message.IInteractiveMessage[] = products.map(product => ({
collectionMessage: {
bizJid: normalizedBizJid,
id: product.productId,
@@ -1211,7 +1207,7 @@ export const generateWAMessageContent = async (
m.messageContextInfo = generated.messageContextInfo
options.logger?.info('Sending carousel as direct interactiveMessage (Pastorini approach, no viewOnce wrapper)')
// Return plain JS object - no fromObject() to avoid corrupting nested carousel structures
return m as WAMessageContent
return m
}
// Check for nativeList
else if (hasNonNullishProperty(message, 'nativeList')) {
@@ -1285,6 +1281,7 @@ export const generateWAMessageContent = async (
} else if (btn.callButton) {
return { index: btn.index, callButton: btn.callButton }
}
return btn
})
@@ -1300,10 +1297,7 @@ export const generateWAMessageContent = async (
options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans')
} else if (hasNonNullishProperty(message, 'sections')) {
// Process list messages - validate limits
validateListSections(
(message as any).sections,
(message as any).buttonText
)
validateListSections((message as any).sections, (message as any).buttonText)
const listMessage: proto.Message.IListMessage = {
title: (message as any).title,
description: (message as any).text,
@@ -1523,21 +1517,22 @@ export const generateWAMessageContent = async (
: proto.Message.ButtonsMessage.HeaderType.VIDEO
// Extract only media properties to avoid type errors
const mediaContent: AnyMediaMessageContent = mediaType === 'image'
? {
image: (message as any).image,
caption: (message as any).caption,
jpegThumbnail: (message as any).jpegThumbnail,
mimetype: (message as any).mimetype
}
: {
video: (message as any).video,
caption: (message as any).caption,
jpegThumbnail: (message as any).jpegThumbnail,
gifPlayback: (message as any).gifPlayback,
ptv: (message as any).ptv,
mimetype: (message as any).mimetype
}
const mediaContent: AnyMediaMessageContent =
mediaType === 'image'
? {
image: (message as any).image,
caption: (message as any).caption,
jpegThumbnail: (message as any).jpegThumbnail,
mimetype: (message as any).mimetype
}
: {
video: (message as any).video,
caption: (message as any).caption,
jpegThumbnail: (message as any).jpegThumbnail,
gifPlayback: (message as any).gifPlayback,
ptv: (message as any).ptv,
mimetype: (message as any).mimetype
}
// Prepare media
const mediaMessage = await prepareWAMessageMedia(mediaContent, options)
@@ -1769,7 +1764,11 @@ export const generateWAMessageFromContent = (
const innerContent = innerMessage[key as Exclude<keyof proto.IMessage, 'conversation'>]
const contextInfo: proto.IContextInfo =
(innerContent && typeof innerContent === 'object' && 'contextInfo' in innerContent && (innerContent as Record<string, unknown>).contextInfo as proto.IContextInfo) || {}
(innerContent &&
typeof innerContent === 'object' &&
'contextInfo' in innerContent &&
((innerContent as Record<string, unknown>).contextInfo as proto.IContextInfo)) ||
{}
contextInfo.participant = jidNormalizedUser(participant!)
contextInfo.stanzaId = quoted.key.id
contextInfo.quotedMessage = quotedMsg
+2 -1
View File
@@ -168,7 +168,8 @@ export class PreKeyManager {
this.logger.debug('PreKeyManager already destroyed')
return
}
this.destroyed = true // ← Set IMMEDIATELY to close race window
this.destroyed = true // ← Set IMMEDIATELY to close race window
this.logger.debug('🗑️ Destroying PreKeyManager')
+10 -19
View File
@@ -18,7 +18,6 @@ import type {
WAMessage,
WAMessageKey
} from '../Types'
import { metrics, recordHistorySyncMessages } from './prometheus-metrics.js'
import { WAMessageStubType } from '../Types'
import { getContentType, normalizeMessageContent } from '../Utils/messages'
import {
@@ -36,6 +35,7 @@ import { aesDecryptGCM, hmacSign } from './crypto'
import { getKeyAuthor, toNumber } from './generics'
import { downloadAndProcessHistorySyncNotification } from './history'
import type { ILogger } from './logger'
import { metrics, recordHistorySyncMessages } from './prometheus-metrics.js'
type ProcessMessageContext = {
shouldProcessHistoryMsg: boolean
@@ -102,8 +102,8 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) =>
// if the sender believed the message being reacted to is not from them
// we've to correct the key to be from them, or some other participant
msgKey.fromMe = !msgKey.fromMe
? areJidsSameUser(msgKey.participant || (msgKey.remoteJid!), meId) ||
areJidsSameUser(msgKey.participant || (msgKey.remoteJid!), meLid)
? areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meId) ||
areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meLid)
: // if the message being reacted to, was from them
// fromMe automatically becomes false
false
@@ -133,6 +133,7 @@ export const normalizeMessageJids = async (
} else {
logger?.debug({ lid: jid }, 'PN not found for inbound LID, keeping LID')
}
return pn || jid
}
@@ -160,9 +161,7 @@ export const isRealMessage = (message: WAMessage) => {
const hasSomeContent = !!getContentType(normalizedContent)
const stubType = message.messageStubType ?? 0
return (
(!!normalizedContent ||
REAL_MSG_STUB_TYPES.has(stubType) ||
REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) &&
(!!normalizedContent || REAL_MSG_STUB_TYPES.has(stubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) &&
hasSomeContent &&
!normalizedContent?.protocolMessage &&
!normalizedContent?.reactionMessage &&
@@ -326,6 +325,7 @@ const processMessage = async (
if (!histNotification) {
break
}
const process = shouldProcessHistoryMsg
const isLatest = !creds.processedHistoryMessages?.length
@@ -363,10 +363,7 @@ const processMessage = async (
'stored LID-PN mappings from history sync'
)
if (result.stored > 0) {
logger?.info(
{ stored: result.stored },
'fallback LID mappings are now available from history sync'
)
logger?.info({ stored: result.stored }, 'fallback LID mappings are now available from history sync')
}
} catch (error) {
logger?.warn({ error }, 'Failed to store LID-PN mappings from history sync')
@@ -476,10 +473,7 @@ const processMessage = async (
// Preserve pushName if not present in PDO response
if (cachedData.pushName && !webMessageInfo.pushName) {
webMessageInfo.pushName = cachedData.pushName
logger?.debug(
{ msgId: webMessageInfo.key?.id },
'CTWA: Restored pushName from cached metadata'
)
logger?.debug({ msgId: webMessageInfo.key?.id }, 'CTWA: Restored pushName from cached metadata')
}
// Preserve participantAlt (LID) if not present in PDO response
@@ -498,10 +492,7 @@ const processMessage = async (
// Preserve original participant if not in PDO response
if (cachedData.participant && webMessageInfo.key && !webMessageInfo.key.participant) {
webMessageInfo.key.participant = cachedData.participant
logger?.debug(
{ msgId: webMessageInfo.key?.id },
'CTWA: Restored participant from cached metadata'
)
logger?.debug({ msgId: webMessageInfo.key?.id }, 'CTWA: Restored participant from cached metadata')
}
// Only use cached timestamp if PDO response doesn't have one
@@ -630,7 +621,7 @@ const processMessage = async (
const meIdNormalised = jidNormalizedUser(meId)
// all jids need to be PN
const eventCreatorKey = creationMsgKey.participant || (creationMsgKey.remoteJid!)
const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid!
const eventCreatorPn = isLidUser(eventCreatorKey)
? await signalRepository.lidMapping.getPNForLID(eventCreatorKey)
: eventCreatorKey
+154 -173
View File
@@ -34,7 +34,7 @@
* @module Utils/prometheus-metrics
*/
import { createServer, IncomingMessage, ServerResponse, Server } from 'http'
import { createServer, IncomingMessage, Server, ServerResponse } from 'http'
import * as os from 'os'
import * as promClient from 'prom-client'
@@ -55,6 +55,7 @@ function configureRegistry(defaultLabels?: Labels): void {
if (registryConfigured) {
return // Already configured, ignore subsequent calls
}
registryConfigured = true
// Allow setting empty labels to clear previous (useful for testing/reconfiguration)
@@ -151,6 +152,7 @@ function parseLabelsFromEnv(envValue: string | undefined): Labels {
if (typeof parsed === 'object' && parsed !== null) {
return parsed as Labels
}
return {}
} catch {
return {}
@@ -173,8 +175,12 @@ export function loadMetricsConfig(): MetricsConfig {
// Separate flag for system metrics (CPU/memory) - independent from collectDefaultMetrics
includeSystem: (process.env.BAILEYS_PROMETHEUS_INCLUDE_SYSTEM ?? process.env.METRICS_INCLUDE_SYSTEM) !== 'false',
// Flag for prom-client default Node.js metrics
collectDefaultMetrics: (process.env.BAILEYS_PROMETHEUS_COLLECT_DEFAULT ?? process.env.METRICS_COLLECT_DEFAULT) !== 'false',
collectIntervalMs: parseInt(process.env.BAILEYS_PROMETHEUS_COLLECT_INTERVAL_MS || process.env.METRICS_COLLECT_INTERVAL_MS || '10000', 10),
collectDefaultMetrics:
(process.env.BAILEYS_PROMETHEUS_COLLECT_DEFAULT ?? process.env.METRICS_COLLECT_DEFAULT) !== 'false',
collectIntervalMs: parseInt(
process.env.BAILEYS_PROMETHEUS_COLLECT_INTERVAL_MS || process.env.METRICS_COLLECT_INTERVAL_MS || '10000',
10
)
}
}
@@ -202,6 +208,7 @@ function stableLabelsToKey(labels: Labels): string {
for (const key of sortedKeys) {
sortedObj[key] = labels[key]!
}
return JSON.stringify(sortedObj)
}
@@ -289,7 +296,7 @@ export class Counter implements BaseMetric {
name: fullName,
help,
labelNames,
registers: [customRegistry], // Use custom registry, not global
registers: [customRegistry] // Use custom registry, not global
})
}
@@ -351,7 +358,7 @@ export class Counter implements BaseMetric {
const metric = await this.promCounter.get()
return metric.values.map(v => ({
labels: v.labels as Labels,
value: v.value,
value: v.value
}))
}
@@ -364,7 +371,7 @@ export class Counter implements BaseMetric {
*/
labels(labels: Labels): { inc: (value?: number) => void } {
return {
inc: (value?: number) => this.inc(labels, value),
inc: (value?: number) => this.inc(labels, value)
}
}
}
@@ -394,7 +401,7 @@ export class Gauge implements BaseMetric {
name: fullName,
help,
labelNames,
registers: [customRegistry], // Use custom registry, not global
registers: [customRegistry] // Use custom registry, not global
})
}
@@ -501,7 +508,7 @@ export class Gauge implements BaseMetric {
const metric = await this.promGauge.get()
return metric.values.map(v => ({
labels: v.labels as Labels,
value: v.value,
value: v.value
}))
}
@@ -520,7 +527,7 @@ export class Gauge implements BaseMetric {
return {
set: (value: number) => this.set(labels, value),
inc: (value?: number) => this.inc(labels, value),
dec: (value?: number) => this.dec(labels, value),
dec: (value?: number) => this.dec(labels, value)
}
}
@@ -566,7 +573,7 @@ export class Histogram implements BaseMetric {
help,
labelNames,
buckets: this.buckets,
registers: [customRegistry], // Use custom registry, not global
registers: [customRegistry] // Use custom registry, not global
})
}
@@ -680,7 +687,7 @@ export class Histogram implements BaseMetric {
labels: vLabels,
buckets: new Map(),
sum: 0,
count: 0,
count: 0
})
}
@@ -718,7 +725,7 @@ export class Histogram implements BaseMetric {
} {
return {
observe: (value: number) => this.observe(labels, value),
startTimer: () => this.startTimer(labels),
startTimer: () => this.startTimer(labels)
}
}
}
@@ -754,7 +761,7 @@ export class Summary implements BaseMetric {
percentiles: this.percentiles,
maxAgeSeconds: options.maxAgeSeconds ?? 600, // 10 min default
ageBuckets: options.ageBuckets ?? 5,
registers: [customRegistry], // Use custom registry, not global
registers: [customRegistry] // Use custom registry, not global
})
}
@@ -852,7 +859,7 @@ export class Summary implements BaseMetric {
labels: vLabels,
values: [],
sum: 0,
count: 0,
count: 0
})
}
@@ -887,7 +894,7 @@ export class Summary implements BaseMetric {
} {
return {
observe: (value: number) => this.observe(labels, value),
startTimer: () => this.startTimer(labels),
startTimer: () => this.startTimer(labels)
}
}
}
@@ -1014,7 +1021,7 @@ export class SystemMetricsCollector {
// CPU tracking for delta calculation
private lastCpuUsage: { user: number; system: number } | null = null
private lastCpuTime: number = 0
private lastCpuTime = 0
// Process metrics
public readonly processUptime: Gauge
@@ -1039,9 +1046,7 @@ export class SystemMetricsCollector {
this.processStartTime = Date.now()
// Initialize process metrics
this.processUptime = registry.register(
new Gauge('process_uptime_seconds', 'Process uptime in seconds')
)
this.processUptime = registry.register(new Gauge('process_uptime_seconds', 'Process uptime in seconds'))
// FIX: Updated description - can exceed 100% on multi-core (100% = 1 core)
this.processCpuUsage = registry.register(
new Gauge('process_cpu_usage_percent', 'Process CPU usage (100% = 1 core fully used)', ['type'])
@@ -1052,29 +1057,15 @@ export class SystemMetricsCollector {
this.processMemoryExternal = registry.register(
new Gauge('process_memory_external_bytes', 'External memory used by C++ objects')
)
this.processMemoryHeapTotal = registry.register(
new Gauge('process_memory_heap_total_bytes', 'Total heap memory')
)
this.processMemoryHeapUsed = registry.register(
new Gauge('process_memory_heap_used_bytes', 'Used heap memory')
)
this.processMemoryRss = registry.register(
new Gauge('process_memory_rss_bytes', 'Resident set size')
)
this.processMemoryHeapTotal = registry.register(new Gauge('process_memory_heap_total_bytes', 'Total heap memory'))
this.processMemoryHeapUsed = registry.register(new Gauge('process_memory_heap_used_bytes', 'Used heap memory'))
this.processMemoryRss = registry.register(new Gauge('process_memory_rss_bytes', 'Resident set size'))
// Initialize system metrics
this.systemCpuUsage = registry.register(
new Gauge('system_cpu_usage_percent', 'System CPU usage percentage')
)
this.systemMemoryTotal = registry.register(
new Gauge('system_memory_total_bytes', 'Total system memory')
)
this.systemMemoryFree = registry.register(
new Gauge('system_memory_free_bytes', 'Free system memory')
)
this.systemLoadAverage = registry.register(
new Gauge('system_load_average', 'System load average', ['period'])
)
this.systemCpuUsage = registry.register(new Gauge('system_cpu_usage_percent', 'System CPU usage percentage'))
this.systemMemoryTotal = registry.register(new Gauge('system_memory_total_bytes', 'Total system memory'))
this.systemMemoryFree = registry.register(new Gauge('system_memory_free_bytes', 'Free system memory'))
this.systemLoadAverage = registry.register(new Gauge('system_load_average', 'System load average', ['period']))
// Event loop lag - use different name to avoid conflict with collectDefaultMetrics
// prom-client's collectDefaultMetrics creates nodejs_eventloop_lag_seconds
@@ -1227,14 +1218,13 @@ export class MetricsServer {
// Cache the promise so concurrent calls get the same one
this.startPromise = new Promise<void>((resolve, reject) => {
// Enable prom-client's default Node.js metrics collection (only once to prevent memory leak)
if (this.config.collectDefaultMetrics && !defaultMetricsCollected) {
defaultMetricsCollected = true
promClient.collectDefaultMetrics({
prefix: this.config.prefix ? `${this.config.prefix}_` : '',
labels: this.config.defaultLabels,
register: customRegistry, // Use custom registry, not global
register: customRegistry // Use custom registry, not global
})
}
@@ -1257,11 +1247,13 @@ export class MetricsServer {
} catch {
// Malformed URL - return 400 Bad Request
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
error: 'Bad Request',
message: 'Malformed URL',
timestamp: new Date().toISOString()
}))
res.end(
JSON.stringify({
error: 'Bad Request',
message: 'Malformed URL',
timestamp: new Date().toISOString()
})
)
return
}
@@ -1279,11 +1271,13 @@ export class MetricsServer {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`[Prometheus] Error collecting metrics: ${errorMessage}`)
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
error: 'Failed to collect metrics',
message: errorMessage,
timestamp: new Date().toISOString()
}))
res.end(
JSON.stringify({
error: 'Failed to collect metrics',
message: errorMessage,
timestamp: new Date().toISOString()
})
)
}
} else if (pathname === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' })
@@ -1294,7 +1288,7 @@ export class MetricsServer {
}
})
this.server.on('error', (error) => {
this.server.on('error', error => {
this.startPromise = null // Reset so retry is possible
this.server = null
reject(error)
@@ -1302,13 +1296,14 @@ export class MetricsServer {
// FIX: Use configurable host instead of hardcoded 0.0.0.0
this.server.listen(this.config.port, this.config.host, () => {
const labelsInfo = Object.keys(this.config.defaultLabels).length > 0
? ` with labels: ${JSON.stringify(this.config.defaultLabels)}`
: ''
const securityNote = this.config.host === '0.0.0.0'
? ' (WARNING: exposed on all interfaces)'
: ''
console.log(`[Prometheus] Metrics server listening on http://${this.config.host}:${this.config.port}${this.config.path}${labelsInfo}${securityNote}`)
const labelsInfo =
Object.keys(this.config.defaultLabels).length > 0
? ` with labels: ${JSON.stringify(this.config.defaultLabels)}`
: ''
const securityNote = this.config.host === '0.0.0.0' ? ' (WARNING: exposed on all interfaces)' : ''
console.log(
`[Prometheus] Metrics server listening on http://${this.config.host}:${this.config.port}${this.config.path}${labelsInfo}${securityNote}`
)
resolve()
})
})
@@ -1321,7 +1316,7 @@ export class MetricsServer {
* FIX: Clears startPromise to allow restart after stop
*/
stop(): Promise<void> {
return new Promise((resolve) => {
return new Promise(resolve => {
// FIX: Clear cached promise so start() can be called again
this.startPromise = null
@@ -1389,24 +1384,30 @@ export const metrics = {
new Counter('reconnect_attempts_total', 'Total reconnection attempts', ['reason'])
),
connectionLatency: baileysMetrics.register(
new Histogram('connection_latency_ms', 'Connection establishment latency in ms', [], [100, 250, 500, 1000, 2500, 5000, 10000])
),
activeConnections: baileysMetrics.register(
new Gauge('active_connections', 'Number of active WhatsApp connections')
new Histogram(
'connection_latency_ms',
'Connection establishment latency in ms',
[],
[100, 250, 500, 1000, 2500, 5000, 10000]
)
),
activeConnections: baileysMetrics.register(new Gauge('active_connections', 'Number of active WhatsApp connections')),
connectionErrors: baileysMetrics.register(
new Counter('connection_errors_total', 'Total connection errors', ['error_type'])
),
// ========== Message Metrics ==========
messagesSent: baileysMetrics.register(
new Counter('messages_sent_total', 'Total messages sent', ['type'])
),
messagesSent: baileysMetrics.register(new Counter('messages_sent_total', 'Total messages sent', ['type'])),
messagesReceived: baileysMetrics.register(
new Counter('messages_received_total', 'Total messages received', ['type'])
),
messageLatency: baileysMetrics.register(
new Histogram('message_latency_ms', 'Message send latency in ms', ['type'], [10, 50, 100, 250, 500, 1000, 2500, 5000])
new Histogram(
'message_latency_ms',
'Message send latency in ms',
['type'],
[10, 50, 100, 250, 500, 1000, 2500, 5000]
)
),
messageRetries: baileysMetrics.register(
new Counter('message_retries_total', 'Total message retry attempts', ['type'])
@@ -1430,7 +1431,12 @@ export const metrics = {
new Counter('ctwa_messages_recovered_total', 'Total CTWA messages successfully recovered')
),
ctwaRecoveryLatency: baileysMetrics.register(
new Histogram('ctwa_recovery_latency_ms', 'CTWA message recovery latency in ms', [], [500, 1000, 2000, 3000, 5000, 8000, 10000])
new Histogram(
'ctwa_recovery_latency_ms',
'CTWA message recovery latency in ms',
[],
[500, 1000, 2000, 3000, 5000, 8000, 10000]
)
),
ctwaRecoveryFailures: baileysMetrics.register(
new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason'])
@@ -1438,7 +1444,11 @@ export const metrics = {
// ========== Interactive Messages Metrics (EXPERIMENTAL) ==========
interactiveMessagesSent: baileysMetrics.register(
new Counter('interactive_messages_sent_total', 'Total interactive messages sent (buttons/lists/templates/carousel)', ['type'])
new Counter(
'interactive_messages_sent_total',
'Total interactive messages sent (buttons/lists/templates/carousel)',
['type']
)
),
interactiveMessagesSuccess: baileysMetrics.register(
new Counter('interactive_messages_success_total', 'Total interactive messages successfully sent', ['type'])
@@ -1447,13 +1457,16 @@ export const metrics = {
new Counter('interactive_messages_failures_total', 'Total interactive message send failures', ['type', 'reason'])
),
interactiveMessagesLatency: baileysMetrics.register(
new Histogram('interactive_messages_latency_ms', 'Interactive message send latency in ms', ['type'], [100, 250, 500, 1000, 2500, 5000, 10000])
new Histogram(
'interactive_messages_latency_ms',
'Interactive message send latency in ms',
['type'],
[100, 250, 500, 1000, 2500, 5000, 10000]
)
),
// ========== Media Metrics ==========
mediaUploads: baileysMetrics.register(
new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])
),
mediaUploads: baileysMetrics.register(new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])),
mediaDownloads: baileysMetrics.register(
new Counter('media_downloads_total', 'Total media downloads', ['type', 'status'])
),
@@ -1461,16 +1474,17 @@ export const metrics = {
new Histogram('media_size_bytes', 'Media size in bytes', ['type', 'direction'], DEFAULT_SIZE_BUCKETS)
),
mediaLatency: baileysMetrics.register(
new Histogram('media_latency_ms', 'Media upload/download latency in ms', ['type', 'direction'], [100, 500, 1000, 2500, 5000, 10000, 30000])
new Histogram(
'media_latency_ms',
'Media upload/download latency in ms',
['type', 'direction'],
[100, 500, 1000, 2500, 5000, 10000, 30000]
)
),
// ========== Event Buffer Metrics ==========
bufferSize: baileysMetrics.register(
new Gauge('buffer_size', 'Current event buffer size', ['type'])
),
bufferCapacity: baileysMetrics.register(
new Gauge('buffer_capacity', 'Maximum event buffer capacity', ['type'])
),
bufferSize: baileysMetrics.register(new Gauge('buffer_size', 'Current event buffer size', ['type'])),
bufferCapacity: baileysMetrics.register(new Gauge('buffer_capacity', 'Maximum event buffer capacity', ['type'])),
bufferUtilization: baileysMetrics.register(
new Gauge('buffer_utilization_percent', 'Event buffer utilization percentage', ['type'])
),
@@ -1487,7 +1501,12 @@ export const metrics = {
new Counter('events_dropped_total', 'Total events dropped due to buffer full', ['event_type'])
),
bufferFlushLatency: baileysMetrics.register(
new Histogram('buffer_flush_latency_ms', 'Buffer flush operation latency in ms', ['type'], [1, 5, 10, 25, 50, 100, 250])
new Histogram(
'buffer_flush_latency_ms',
'Buffer flush operation latency in ms',
['type'],
[1, 5, 10, 25, 50, 100, 250]
)
),
eventsProcessed: baileysMetrics.register(
new Counter('events_processed_total', 'Total events processed from buffer', ['event_type'])
@@ -1501,9 +1520,7 @@ export const metrics = {
bufferCacheCleanup: baileysMetrics.register(
new Counter('buffer_cache_cleanup_total', 'Total cache cleanup operations')
),
bufferCacheSize: baileysMetrics.register(
new Gauge('buffer_cache_size', 'Current buffer cache size')
),
bufferCacheSize: baileysMetrics.register(new Gauge('buffer_cache_size', 'Current buffer cache size')),
// ========== Adaptive Flush Metrics ==========
adaptiveFlushInterval: baileysMetrics.register(
@@ -1524,45 +1541,29 @@ export const metrics = {
adaptiveHealthStatus: baileysMetrics.register(
new Gauge('adaptive_health_status', 'Adaptive system health status (0=unhealthy, 1=healthy)')
),
adaptiveEventRate: baileysMetrics.register(
new Gauge('adaptive_event_rate', 'Current event rate per second')
),
adaptiveEventRate: baileysMetrics.register(new Gauge('adaptive_event_rate', 'Current event rate per second')),
// ========== Error Metrics ==========
errors: baileysMetrics.register(
new Counter('errors_total', 'Total errors', ['category', 'code'])
),
errorRate: baileysMetrics.register(
new Gauge('error_rate', 'Current error rate per minute', ['category'])
),
errors: baileysMetrics.register(new Counter('errors_total', 'Total errors', ['category', 'code'])),
errorRate: baileysMetrics.register(new Gauge('error_rate', 'Current error rate per minute', ['category'])),
// ========== Retry Metrics ==========
retries: baileysMetrics.register(
new Counter('retries_total', 'Total retries', ['operation'])
),
retryLatency: baileysMetrics.register(
new Histogram('retry_latency_ms', 'Retry latency in ms', ['operation'])
),
retrySuccess: baileysMetrics.register(
new Counter('retry_success_total', 'Successful retries', ['operation'])
),
retries: baileysMetrics.register(new Counter('retries_total', 'Total retries', ['operation'])),
retryLatency: baileysMetrics.register(new Histogram('retry_latency_ms', 'Retry latency in ms', ['operation'])),
retrySuccess: baileysMetrics.register(new Counter('retry_success_total', 'Successful retries', ['operation'])),
retryExhausted: baileysMetrics.register(
new Counter('retry_exhausted_total', 'Exhausted retry attempts', ['operation'])
),
// ========== Socket Metrics ==========
socketEvents: baileysMetrics.register(
new Counter('socket_events_total', 'Total socket events', ['event'])
),
socketEvents: baileysMetrics.register(new Counter('socket_events_total', 'Total socket events', ['event'])),
socketLatency: baileysMetrics.register(
new Histogram('socket_latency_ms', 'Socket operation latency in ms', ['operation'])
),
socketBytesReceived: baileysMetrics.register(
new Counter('socket_bytes_received_total', 'Total bytes received through socket')
),
socketBytesSent: baileysMetrics.register(
new Counter('socket_bytes_sent_total', 'Total bytes sent through socket')
),
socketBytesSent: baileysMetrics.register(new Counter('socket_bytes_sent_total', 'Total bytes sent through socket')),
socketReconnects: baileysMetrics.register(
new Counter('socket_reconnects_total', 'Total socket reconnection attempts', ['reason'])
),
@@ -1594,12 +1595,8 @@ export const metrics = {
encryptionLatency: baileysMetrics.register(
new Histogram('encryption_latency_ms', 'Encryption operation latency in ms', ['operation'], [1, 5, 10, 25, 50, 100])
),
keyExchanges: baileysMetrics.register(
new Counter('key_exchanges_total', 'Total key exchange operations', ['type'])
),
preKeyCount: baileysMetrics.register(
new Gauge('prekey_count', 'Current prekey count')
),
keyExchanges: baileysMetrics.register(new Counter('key_exchanges_total', 'Total key exchange operations', ['type'])),
preKeyCount: baileysMetrics.register(new Gauge('prekey_count', 'Current prekey count')),
// ========== Signal Identity Metrics ==========
/** Total identity key changes detected (contact reinstalled WhatsApp) */
@@ -1623,7 +1620,12 @@ export const metrics = {
),
/** Identity key operations latency */
signalIdentityKeyOperations: baileysMetrics.register(
new Histogram('signal_identity_key_operations_ms', 'Identity key operation latency in ms', ['operation'], [1, 5, 10, 25, 50, 100])
new Histogram(
'signal_identity_key_operations_ms',
'Identity key operation latency in ms',
['operation'],
[1, 5, 10, 25, 50, 100]
)
),
/** Current identity key cache size */
signalIdentityKeyCacheSize: baileysMetrics.register(
@@ -1631,21 +1633,13 @@ export const metrics = {
),
// ========== Cache Metrics ==========
cacheHits: baileysMetrics.register(
new Counter('cache_hits_total', 'Total cache hits', ['cache'])
),
cacheMisses: baileysMetrics.register(
new Counter('cache_misses_total', 'Total cache misses', ['cache'])
),
cacheSize: baileysMetrics.register(
new Gauge('cache_size', 'Current cache size', ['cache'])
),
cacheHits: baileysMetrics.register(new Counter('cache_hits_total', 'Total cache hits', ['cache'])),
cacheMisses: baileysMetrics.register(new Counter('cache_misses_total', 'Total cache misses', ['cache'])),
cacheSize: baileysMetrics.register(new Gauge('cache_size', 'Current cache size', ['cache'])),
cacheEvictions: baileysMetrics.register(
new Counter('cache_evictions_total', 'Total cache evictions', ['cache', 'reason'])
),
cacheHitRate: baileysMetrics.register(
new Gauge('cache_hit_rate', 'Cache hit rate (0-1)', ['cache'])
),
cacheHitRate: baileysMetrics.register(new Gauge('cache_hit_rate', 'Cache hit rate (0-1)', ['cache'])),
// ========== Query Metrics ==========
queryLatency: baileysMetrics.register(
@@ -1654,17 +1648,13 @@ export const metrics = {
queryCount: baileysMetrics.register(
new Counter('query_count_total', 'Total queries executed', ['query_type', 'status'])
),
queryTimeouts: baileysMetrics.register(
new Counter('query_timeouts_total', 'Total query timeouts', ['query_type'])
),
queryTimeouts: baileysMetrics.register(new Counter('query_timeouts_total', 'Total query timeouts', ['query_type'])),
// ========== Presence Metrics ==========
presenceUpdates: baileysMetrics.register(
new Counter('presence_updates_total', 'Total presence updates received', ['type'])
),
presenceSubscriptions: baileysMetrics.register(
new Gauge('presence_subscriptions', 'Current presence subscriptions')
),
presenceSubscriptions: baileysMetrics.register(new Gauge('presence_subscriptions', 'Current presence subscriptions')),
// ========== Group Metrics ==========
groupOperations: baileysMetrics.register(
@@ -1682,8 +1672,13 @@ export const metrics = {
new Counter('history_sync_messages_total', 'Total messages synced from history')
),
historySyncDuration: baileysMetrics.register(
new Histogram('history_sync_duration_ms', 'History sync duration in ms', ['type'], [1000, 5000, 10000, 30000, 60000])
),
new Histogram(
'history_sync_duration_ms',
'History sync duration in ms',
['type'],
[1000, 5000, 10000, 30000, 60000]
)
)
}
// ============================================
@@ -1764,7 +1759,10 @@ export class PrometheusMetricsManager {
* Helper to create HTTP metrics endpoint handler
*/
export function createMetricsHandler(registry: MetricsRegistry = baileysMetrics) {
return async (_req: unknown, res: { setHeader: (name: string, value: string) => void; end: (body: string) => void }) => {
return async (
_req: unknown,
res: { setHeader: (name: string, value: string) => void; end: (body: string) => void }
) => {
const metricsOutput = await registry.getMetricsOutput()
res.setHeader('Content-Type', registry.contentType())
res.end(metricsOutput)
@@ -1785,17 +1783,14 @@ export function createExpressMetricsMiddleware(registry: MetricsRegistry = baile
/**
* Track operation duration using histogram
*/
export function trackDuration<T>(
histogram: Histogram,
labels: Labels,
operation: () => T
): T {
export function trackDuration<T>(histogram: Histogram, labels: Labels, operation: () => T): T {
const endTimer = histogram.startTimer(labels)
try {
const result = operation()
if (result instanceof Promise) {
return result.finally(() => endTimer()) as T
}
endTimer()
return result
} catch (error) {
@@ -1826,14 +1821,10 @@ export async function trackDurationAsync<T>(
/**
* Increment counter with automatic error tracking
*/
export function trackOperation(
successCounter: Counter,
errorCounter: Counter,
labels: Labels
) {
export function trackOperation(successCounter: Counter, errorCounter: Counter, labels: Labels) {
return {
success: () => successCounter.inc(labels),
failure: (errorCode?: string) => errorCounter.inc({ ...labels, code: errorCode || 'unknown' }),
failure: (errorCode?: string) => errorCounter.inc({ ...labels, code: errorCode || 'unknown' })
}
}
@@ -1860,24 +1851,13 @@ function getEventBufferMetrics() {
[],
[1, 5, 10, 25, 50, 100, 250, 500, 1000]
),
bufferCurrentSize: new Gauge(
'buffer_current_size',
'Current number of events in buffer'
),
bufferPeakSize: new Gauge(
'buffer_peak_size',
'Peak buffer size reached'
),
bufferHistoryCacheSize: new Gauge(
'buffer_history_cache_size',
'Current size of history cache'
),
bufferLruCleanups: new Counter(
'buffer_lru_cleanups_total',
'Total number of LRU cache cleanups performed'
)
bufferCurrentSize: new Gauge('buffer_current_size', 'Current number of events in buffer'),
bufferPeakSize: new Gauge('buffer_peak_size', 'Peak buffer size reached'),
bufferHistoryCacheSize: new Gauge('buffer_history_cache_size', 'Current size of history cache'),
bufferLruCleanups: new Counter('buffer_lru_cleanups_total', 'Total number of LRU cache cleanups performed')
}
}
return eventBufferMetrics
}
@@ -1885,7 +1865,7 @@ function getEventBufferMetrics() {
* Record an event being buffered
* Used by event-buffer.ts for metrics integration
*/
export function recordEventBuffered(eventType: string, count: number = 1): void {
export function recordEventBuffered(eventType: string, count = 1): void {
try {
// Use the main metrics object which has eventsBuffered with label ['event_type']
metrics.eventsBuffered?.inc({ event_type: eventType }, count)
@@ -2065,7 +2045,7 @@ export function recordConnectionAttempt(status: 'success' | 'failure'): void {
/**
* Record a message sent
*/
export function recordMessageSent(type: string = 'text'): void {
export function recordMessageSent(type = 'text'): void {
try {
metrics.messagesSent?.inc({ type })
} catch {
@@ -2076,7 +2056,7 @@ export function recordMessageSent(type: string = 'text'): void {
/**
* Record a message received
*/
export function recordMessageReceived(type: string = 'text'): void {
export function recordMessageReceived(type = 'text'): void {
try {
metrics.messagesReceived?.inc({ type })
} catch {
@@ -2087,7 +2067,7 @@ export function recordMessageReceived(type: string = 'text'): void {
/**
* Record a message retry attempt
*/
export function recordMessageRetry(type: string = 'text'): void {
export function recordMessageRetry(type = 'text'): void {
try {
metrics.messageRetries?.inc({ type })
} catch {
@@ -2098,7 +2078,7 @@ export function recordMessageRetry(type: string = 'text'): void {
/**
* Record a message failure
*/
export function recordMessageFailure(type: string = 'text', reason: string = 'unknown'): void {
export function recordMessageFailure(type = 'text', reason = 'unknown'): void {
try {
metrics.messageFailures?.inc({ type, reason })
} catch {
@@ -2109,7 +2089,7 @@ export function recordMessageFailure(type: string = 'text', reason: string = 'un
/**
* Update messages queued gauge
*/
export function setMessagesQueued(count: number, priority: string = 'normal'): void {
export function setMessagesQueued(count: number, priority = 'normal'): void {
try {
metrics.messagesQueued?.set({ priority }, count)
} catch {
@@ -2120,7 +2100,7 @@ export function setMessagesQueued(count: number, priority: string = 'normal'): v
/**
* Record history sync messages
*/
export function recordHistorySyncMessages(count: number = 1): void {
export function recordHistorySyncMessages(count = 1): void {
try {
metrics.historySyncMessages?.inc({}, count)
} catch {
@@ -2144,6 +2124,7 @@ export function getMetricsManager(config?: Partial<MetricsConfig>): PrometheusMe
if (!globalMetricsManager) {
globalMetricsManager = new PrometheusMetricsManager(config)
}
return globalMetricsManager
}
@@ -2231,7 +2212,7 @@ if (metricsConfig.enabled) {
.then(() => {
console.log('[Prometheus] Auto-initialized metrics server successfully')
})
.catch((error) => {
.catch(error => {
console.error('[Prometheus] Failed to auto-initialize metrics server:', error)
})
})
+45 -55
View File
@@ -14,8 +14,8 @@
*/
import { EventEmitter } from 'events'
import { metrics } from './prometheus-metrics.js'
import type { CircuitBreaker } from './circuit-breaker.js'
import { metrics } from './prometheus-metrics.js'
/**
* Retry configuration with custom progressive backoff
@@ -190,6 +190,7 @@ function fibonacciNumber(n: number): number {
a = b
b = c
}
return b
}
@@ -205,6 +206,7 @@ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
if (signal && abortHandler) {
signal.removeEventListener('abort', abortHandler)
}
resolve()
}, ms)
@@ -228,11 +230,7 @@ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
/**
* Execute operation with timeout
*/
async function executeWithTimeout<T>(
operation: () => Promise<T>,
timeout: number,
signal?: AbortSignal
): Promise<T> {
async function executeWithTimeout<T>(operation: () => Promise<T>, timeout: number, signal?: AbortSignal): Promise<T> {
return new Promise<T>((resolve, reject) => {
let completed = false
@@ -250,14 +248,14 @@ async function executeWithTimeout<T>(
}
operation()
.then((result) => {
.then(result => {
if (!completed) {
completed = true
clearTimeout(timer)
resolve(result)
}
})
.catch((error) => {
.catch(error => {
if (!completed) {
completed = true
clearTimeout(timer)
@@ -289,14 +287,14 @@ export async function retry<T>(
onRetry: options.onRetry ?? (() => {}),
onSuccess: options.onSuccess ?? (() => {}),
onFailure: options.onFailure ?? (() => {}),
abortSignal: options.abortSignal,
abortSignal: options.abortSignal
}
const context: RetryContext = {
attempt: 0,
maxAttempts: config.maxAttempts,
startTime: Date.now(),
aborted: false,
aborted: false
}
let lastError: Error | undefined
@@ -325,11 +323,7 @@ export async function retry<T>(
let result: T
if (config.timeout) {
result = await executeWithTimeout(
() => Promise.resolve(operation(context)),
config.timeout,
config.abortSignal
)
result = await executeWithTimeout(() => Promise.resolve(operation(context)), config.timeout, config.abortSignal)
} else {
result = await operation(context)
}
@@ -398,21 +392,18 @@ export async function retryWithResult<T>(
let lastAttemptStart = startTime
try {
const result = await retry(
(context) => {
attempts = context.attempt
lastAttemptStart = Date.now()
return operation(context)
},
options
)
const result = await retry(context => {
attempts = context.attempt
lastAttemptStart = Date.now()
return operation(context)
}, options)
return {
success: true,
result,
attempts,
totalDuration: Date.now() - startTime,
lastAttemptDuration: Date.now() - lastAttemptStart,
lastAttemptDuration: Date.now() - lastAttemptStart
}
} catch (error) {
return {
@@ -420,7 +411,7 @@ export async function retryWithResult<T>(
error: error as Error,
attempts,
totalDuration: Date.now() - startTime,
lastAttemptDuration: Date.now() - lastAttemptStart,
lastAttemptDuration: Date.now() - lastAttemptStart
}
}
}
@@ -429,10 +420,7 @@ export async function retryWithResult<T>(
* Factory to create configured retry function
*/
export function createRetrier(defaultOptions: RetryOptions = {}) {
return <T>(
operation: (context: RetryContext) => T | Promise<T>,
options?: RetryOptions
): Promise<T> => {
return <T>(operation: (context: RetryContext) => T | Promise<T>, options?: RetryOptions): Promise<T> => {
return retry(operation, { ...defaultOptions, ...options })
}
}
@@ -450,10 +438,10 @@ export function withRetry(options: RetryOptions = {}) {
if (!originalMethod) return descriptor
descriptor.value = async function (...args: unknown[]): Promise<unknown> {
return retry(
() => originalMethod.apply(this, args),
{ ...options, operationName: options.operationName || propertyKey }
)
return retry(() => originalMethod.apply(this, args), {
...options,
operationName: options.operationName || propertyKey
})
}
return descriptor
@@ -463,7 +451,7 @@ export function withRetry(options: RetryOptions = {}) {
/**
* Wrapper for function with retry
*/
export function retryable<T extends (...args: unknown[]) => unknown>(
export function retryable<T extends(...args: unknown[]) => unknown>(
fn: T,
options: RetryOptions = {}
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
@@ -498,10 +486,10 @@ export class RetryManager extends EventEmitter {
const abortController = new AbortController()
const mergedOptions = { ...this.defaultOptions, ...options, abortSignal: abortController.signal }
const retryPromise = retry((context) => {
const retryPromise = retry(context => {
this.activeRetries.set(id, {
cancel: () => abortController.abort(),
context,
context
})
this.emit('attempt', { id, attempt: context.attempt })
return operation(context)
@@ -530,6 +518,7 @@ export class RetryManager extends EventEmitter {
this.emit('cancelled', { id })
return true
}
return false
}
@@ -541,6 +530,7 @@ export class RetryManager extends EventEmitter {
active.cancel()
this.emit('cancelled', { id })
}
this.activeRetries.clear()
}
@@ -579,22 +569,22 @@ export const retryPredicates = {
/** Retry only on network errors */
onNetworkError: (error: Error) => {
const networkErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN']
return networkErrors.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
return networkErrors.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry only on specific errors */
onErrorCodes:
(codes: string[]) =>
(error: Error): boolean => {
return codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
(error: Error): boolean => {
return codes.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry except on specific errors */
exceptErrorCodes:
(codes: string[]) =>
(error: Error): boolean => {
return !codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
(error: Error): boolean => {
return !codes.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry on HTTP 5xx errors or timeout */
onServerError: (error: Error) => {
@@ -611,16 +601,16 @@ export const retryPredicates = {
/** Combine multiple predicates with OR */
or:
(...predicates: Array<(error: Error, attempt: number) => boolean>) =>
(error: Error, attempt: number): boolean => {
return predicates.some((p) => p(error, attempt))
},
(error: Error, attempt: number): boolean => {
return predicates.some(p => p(error, attempt))
},
/** Combine multiple predicates with AND */
and:
(...predicates: Array<(error: Error, attempt: number) => boolean>) =>
(error: Error, attempt: number): boolean => {
return predicates.every((p) => p(error, attempt))
},
(error: Error, attempt: number): boolean => {
return predicates.every(p => p(error, attempt))
}
}
/**
@@ -633,7 +623,7 @@ export const retryConfigs = {
baseDelay: 100,
maxDelay: 5000,
backoffStrategy: 'exponential' as const,
jitter: 0.2,
jitter: 0.2
},
/** Conservative retry (few attempts, long delays) */
@@ -642,7 +632,7 @@ export const retryConfigs = {
baseDelay: 2000,
maxDelay: 60000,
backoffStrategy: 'exponential' as const,
jitter: 0.1,
jitter: 0.1
},
/** Fast retry (for short operations) */
@@ -651,7 +641,7 @@ export const retryConfigs = {
baseDelay: 50,
maxDelay: 1000,
backoffStrategy: 'linear' as const,
jitter: 0.05,
jitter: 0.05
},
/** Retry for network operations */
@@ -661,7 +651,7 @@ export const retryConfigs = {
maxDelay: 30000,
backoffStrategy: 'exponential' as const,
jitter: 0.1,
shouldRetry: retryPredicates.onNetworkError,
shouldRetry: retryPredicates.onNetworkError
},
/**
@@ -679,8 +669,8 @@ export const retryConfigs = {
baseDelay: 1000, // = RETRY_BACKOFF_DELAYS[0]
maxDelay: 20000, // = RETRY_BACKOFF_DELAYS[4]
backoffStrategy: 'stepped' as const,
jitter: 0.15, // = RETRY_JITTER_FACTOR
},
jitter: 0.15 // = RETRY_JITTER_FACTOR
}
}
/**
+21 -15
View File
@@ -1,13 +1,13 @@
import { Boom } from '@hapi/boom'
import { createHash } from 'crypto'
import { zipSync } from 'fflate'
import { promises as fs } from 'fs'
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js'
import type { MediaType } from '../Defaults/index.js'
import type { Sticker, StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js'
import type { ILogger } from './logger.js'
import { generateMessageIDV2 } from './generics.js'
import { getImageProcessingLibrary, encryptedStream } from './messages-media.js'
import type { ILogger } from './logger.js'
import { encryptedStream, getImageProcessingLibrary } from './messages-media.js'
/**
* Verifica se um buffer é um arquivo WebP válido
@@ -86,7 +86,7 @@ export const isAnimatedWebP = (buffer: Buffer): boolean => {
if (chunkFourCC === 'VP8X' && offset + 8 < buffer.length) {
const flags = buffer[offset + 8]
// Bit 1 (0x02) = animation flag
if (flags && (flags & 0x02)) return true
if (flags && flags & 0x02) return true
}
// Animation chunks
@@ -156,7 +156,7 @@ const generateSha256Hash = (buffer: Buffer): string => {
.digest('base64')
.replace(/\+/g, '-') // + becomes -
.replace(/\//g, '_') // / becomes _ (CRITICAL: different from + mapping!)
.replace(/=/g, '') // Remove padding
.replace(/=/g, '') // Remove padding
}
/**
@@ -190,6 +190,7 @@ const mediaToBuffer = async (
statusCode: 413
})
}
return media
} else if (typeof media === 'object' && 'url' in media) {
const url = media.url.toString()
@@ -201,6 +202,7 @@ const mediaToBuffer = async (
if (!base64Data) {
throw new Boom(`Invalid data URL for ${context}: missing base64 data`, { statusCode: 400 })
}
const buffer = Buffer.from(base64Data, 'base64')
// SECURITY: Validate buffer size
@@ -485,7 +487,11 @@ export const prepareStickerPackMessage = async (
if (compressed70.length <= MAX_STICKER_SIZE) {
webpBuffer = compressed70
logger?.info(
{ index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed70.length / 1024).toFixed(2) },
{
index: i,
originalKB: (webpBuffer.length / 1024).toFixed(2),
compressedKB: (compressed70.length / 1024).toFixed(2)
},
`Sticker ${i + 1} compressed successfully (quality 70)`
)
} else {
@@ -495,7 +501,11 @@ export const prepareStickerPackMessage = async (
if (compressed50.length <= MAX_STICKER_SIZE) {
webpBuffer = compressed50
logger?.info(
{ index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed50.length / 1024).toFixed(2) },
{
index: i,
originalKB: (webpBuffer.length / 1024).toFixed(2),
compressedKB: (compressed50.length / 1024).toFixed(2)
},
`Sticker ${i + 1} compressed successfully (quality 50)`
)
} else {
@@ -587,10 +597,7 @@ export const prepareStickerPackMessage = async (
}
}
logger?.debug(
{ fileName, mergedEmojis, duplicateCount },
'Duplicate sticker detected - merged metadata'
)
logger?.debug({ fileName, mergedEmojis, duplicateCount }, 'Duplicate sticker detected - merged metadata')
} else {
// New sticker - add to ZIP and create metadata
stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 as 0 }]
@@ -691,10 +698,9 @@ export const prepareStickerPackMessage = async (
const lib = await getImageProcessingLibrary()
if (!lib?.sharp) {
throw new Boom(
'Sharp library is required for thumbnail generation. Install with: yarn add sharp',
{ statusCode: 400 }
)
throw new Boom('Sharp library is required for thumbnail generation. Install with: yarn add sharp', {
statusCode: 400
})
}
thumbnailBuffer = await lib.sharp
+51 -48
View File
@@ -38,7 +38,7 @@ export const LOG_LEVEL_VALUES: Record<LogLevel, number> = {
warn: 40,
error: 50,
fatal: 60,
silent: 100,
silent: 100
}
/**
@@ -111,7 +111,7 @@ export function loadLoggerConfig(): Partial<StructuredLoggerConfig> {
: undefined,
enableAsyncQueue: process.env.BAILEYS_LOG_ASYNC === 'true',
enableMetrics: process.env.BAILEYS_LOG_METRICS === 'true',
includeStackTrace: process.env.BAILEYS_LOG_STACK_TRACE !== 'false',
includeStackTrace: process.env.BAILEYS_LOG_STACK_TRACE !== 'false'
}
}
@@ -195,7 +195,7 @@ const DEFAULT_REDACT_FIELDS = [
'auth',
'credentials',
'privateKey',
'private_key',
'private_key'
]
// ============================================================================
@@ -224,6 +224,7 @@ class RateLimiter {
this.tokens--
return true
}
return false
}
@@ -249,8 +250,8 @@ class RateLimiter {
* Circuit breaker for external hook protection
*/
class CircuitBreaker {
private failures: number = 0
private lastFailure: number = 0
private failures = 0
private lastFailure = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
constructor(
@@ -295,6 +296,7 @@ class CircuitBreaker {
if (this.state === 'open' && Date.now() - this.lastFailure > this.resetTimeoutMs) {
this.state = 'half-open'
}
return this.state
}
@@ -312,8 +314,8 @@ class CircuitBreaker {
*/
class AsyncLogQueue {
private queue: Array<() => void> = []
private processing: boolean = false
private destroyed: boolean = false
private processing = false
private destroyed = false
enqueue(task: () => void): void {
if (this.destroyed) return
@@ -334,11 +336,13 @@ class AsyncLogQueue {
// Silently ignore errors
}
}
// Yield to event loop periodically
if (this.queue.length > 0 && this.queue.length % 10 === 0) {
await new Promise(resolve => setImmediate(resolve))
}
}
this.processing = false
}
@@ -388,7 +392,7 @@ export class StructuredLogger implements ILogger {
private config: ResolvedLoggerConfig
private metrics: LoggerMetrics
private childContext: Record<string, unknown> = {}
private destroyed: boolean = false
private destroyed = false
private createdAt: number
// Buffer for batch writes
@@ -405,8 +409,8 @@ export class StructuredLogger implements ILogger {
private asyncQueue: AsyncLogQueue | null = null
// Performance tracking
private totalProcessingTime: number = 0
private processedLogs: number = 0
private totalProcessingTime = 0
private processedLogs = 0
// Metrics module (lazy loaded)
// NOTE: Currently no metrics are recorded to this module - it's loaded but unused
@@ -433,7 +437,7 @@ export class StructuredLogger implements ILogger {
enableAsyncQueue: config.enableAsyncQueue ?? envConfig.enableAsyncQueue ?? false,
circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5,
circuitBreakerResetMs: config.circuitBreakerResetMs ?? 30000,
enableMetrics: config.enableMetrics ?? envConfig.enableMetrics ?? false,
enableMetrics: config.enableMetrics ?? envConfig.enableMetrics ?? false
}
this.metrics = {
@@ -445,14 +449,14 @@ export class StructuredLogger implements ILogger {
warn: 0,
error: 0,
fatal: 0,
silent: 0,
silent: 0
},
errorsCount: 0,
droppedLogs: 0,
bufferFlushes: 0,
hookFailures: 0,
circuitBreakerTrips: 0,
avgProcessingTimeMs: 0,
avgProcessingTimeMs: 0
}
// Initialize rate limiter
@@ -462,10 +466,7 @@ export class StructuredLogger implements ILogger {
// Initialize circuit breaker for external hook
if (this.config.externalHook) {
this.circuitBreaker = new CircuitBreaker(
this.config.circuitBreakerThreshold,
this.config.circuitBreakerResetMs
)
this.circuitBreaker = new CircuitBreaker(this.config.circuitBreakerThreshold, this.config.circuitBreakerResetMs)
}
// Initialize async queue
@@ -482,12 +483,14 @@ export class StructuredLogger implements ILogger {
// Load metrics module if enabled (currently unused but loaded for future use)
if (this.config.enableMetrics) {
import('./prometheus-metrics').then(m => {
this.metricsModule = m
this.debug('📊 Prometheus metrics loaded for logger')
}).catch(() => {
// Metrics module not available - silent fail
})
import('./prometheus-metrics')
.then(m => {
this.metricsModule = m
this.debug('📊 Prometheus metrics loaded for logger')
})
.catch(() => {
// Metrics module not available - silent fail
})
}
}
@@ -520,7 +523,7 @@ export class StructuredLogger implements ILogger {
child(obj: Record<string, unknown>): StructuredLogger {
const childLogger = new StructuredLogger({
...this.config,
context: { ...this.config.context, ...this.childContext, ...obj },
context: { ...this.config.context, ...this.childContext, ...obj }
})
childLogger.childContext = { ...this.childContext, ...obj }
return childLogger
@@ -569,14 +572,16 @@ export class StructuredLogger implements ILogger {
// External hook with circuit breaker
const externalHook = this.config.externalHook
if (externalHook && this.circuitBreaker) {
this.circuitBreaker.execute(async () => {
await Promise.resolve(externalHook(entry))
}).catch(() => {
this.metrics.hookFailures++
if (this.circuitBreaker?.getState() === 'open') {
this.metrics.circuitBreakerTrips++
}
})
this.circuitBreaker
.execute(async () => {
await Promise.resolve(externalHook(entry))
})
.catch(() => {
this.metrics.hookFailures++
if (this.circuitBreaker?.getState() === 'open') {
this.metrics.circuitBreakerTrips++
}
})
}
// Track processing time
@@ -623,10 +628,11 @@ export class StructuredLogger implements ILogger {
if (this.config.includeStackTrace && obj.stack) {
stack = obj.stack
}
data = {
errorName: obj.name,
errorMessage: obj.message,
...(obj as unknown as Record<string, unknown>),
...(obj as unknown as Record<string, unknown>)
}
} else if (typeof obj === 'object' && obj !== null) {
data = this.sanitize(obj as Record<string, unknown>)
@@ -651,7 +657,7 @@ export class StructuredLogger implements ILogger {
data,
stack,
correlationId,
durationMs,
durationMs
}
}
@@ -664,7 +670,7 @@ export class StructuredLogger implements ILogger {
for (const [key, value] of Object.entries(obj)) {
const lowerKey = key.toLowerCase()
if (this.config.redactFields.some((field) => lowerKey.includes(field.toLowerCase()))) {
if (this.config.redactFields.some(field => lowerKey.includes(field.toLowerCase()))) {
sanitized[key] = '[REDACTED]'
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
sanitized[key] = this.sanitize(value as Record<string, unknown>)
@@ -723,7 +729,7 @@ export class StructuredLogger implements ILogger {
entry.name ? `[${entry.name}]` : '',
entry.correlationId ? `[${entry.correlationId}]` : '',
entry.message,
entry.durationMs !== undefined ? `(${entry.durationMs}ms)` : '',
entry.durationMs !== undefined ? `(${entry.durationMs}ms)` : ''
]
let text = parts.filter(Boolean).join(' ')
@@ -782,11 +788,7 @@ export class StructuredLogger implements ILogger {
/**
* Log operation with duration tracking
*/
logOperation<T>(
operationName: string,
operation: () => T | Promise<T>,
level: LogLevel = 'info'
): T | Promise<T> {
logOperation<T>(operationName: string, operation: () => T | Promise<T>, level: LogLevel = 'info'): T | Promise<T> {
const startTime = Date.now()
const contextLogger = this.child({ operation: operationName })
@@ -808,7 +810,7 @@ export class StructuredLogger implements ILogger {
const result = operation()
if (result instanceof Promise) {
return result.then(handleResult).catch(handleError) as Promise<T>
return result.then(handleResult).catch(handleError)
}
return handleResult(result)
@@ -835,7 +837,7 @@ export class StructuredLogger implements ILogger {
circuitBreakerState: this.circuitBreaker?.getState() ?? 'closed',
queueSize: this.asyncQueue?.getSize() ?? 0,
createdAt: this.createdAt,
uptimeMs: Date.now() - this.createdAt,
uptimeMs: Date.now() - this.createdAt
}
}
@@ -852,14 +854,14 @@ export class StructuredLogger implements ILogger {
warn: 0,
error: 0,
fatal: 0,
silent: 0,
silent: 0
},
errorsCount: 0,
droppedLogs: 0,
bufferFlushes: 0,
hookFailures: 0,
circuitBreakerTrips: 0,
avgProcessingTimeMs: 0,
avgProcessingTimeMs: 0
}
this.totalProcessingTime = 0
this.processedLogs = 0
@@ -902,7 +904,7 @@ export class StructuredLogger implements ILogger {
export function createStructuredLogger(config: Partial<StructuredLoggerConfig> = {}): StructuredLogger {
return new StructuredLogger({
level: config.level || 'info',
...config,
...config
})
}
@@ -916,9 +918,10 @@ export function getDefaultLogger(): StructuredLogger {
defaultLogger = createStructuredLogger({
level: 'info',
name: 'baileys',
jsonFormat: process.env.NODE_ENV === 'production',
jsonFormat: process.env.NODE_ENV === 'production'
})
}
return defaultLogger
}
@@ -933,7 +936,7 @@ export function createTimer(): { elapsed: () => number; elapsedMs: () => string
const start = process.hrtime.bigint()
return {
elapsed: () => Number(process.hrtime.bigint() - start) / 1_000_000,
elapsedMs: () => `${(Number(process.hrtime.bigint() - start) / 1_000_000).toFixed(2)}ms`,
elapsedMs: () => `${(Number(process.hrtime.bigint() - start) / 1_000_000).toFixed(2)}ms`
}
}
+23 -30
View File
@@ -12,8 +12,8 @@
* @module Utils/trace-context
*/
import { randomBytes } from 'crypto'
import { AsyncLocalStorage } from 'async_hooks'
import { randomBytes } from 'crypto'
/**
* Trace identifiers
@@ -169,12 +169,12 @@ export function createTraceContext(options: CreateContextOptions = {}): TraceCon
traceId,
spanId,
parentSpanId: options.parentSpanId,
correlationId,
correlationId
},
baggage: options.baggage || {},
spanStack: [],
createdAt: Date.now(),
metadata: options.metadata || {},
metadata: options.metadata || {}
}
}
@@ -193,6 +193,7 @@ export function getOrCreateContext(): TraceContext {
if (existing) {
return existing
}
return createTraceContext()
}
@@ -214,10 +215,7 @@ export function runWithNewContext<T>(options: CreateContextOptions, fn: () => T)
/**
* Execute async function with trace context
*/
export async function runWithContextAsync<T>(
context: TraceContext,
fn: () => Promise<T>
): Promise<T> {
export async function runWithContextAsync<T>(context: TraceContext, fn: () => Promise<T>): Promise<T> {
return traceStorage.run(context, fn)
}
@@ -234,13 +232,13 @@ export function createSpan(options: CreateSpanOptions): Span {
traceId: context?.traceIds.traceId || generateTraceId(),
spanId: generateSpanId(),
parentSpanId: options.asChild && parentSpan ? parentSpan.traceIds.spanId : undefined,
correlationId: context?.traceIds.correlationId || generateCorrelationId(),
correlationId: context?.traceIds.correlationId || generateCorrelationId()
},
startTime: Date.now(),
status: 'unset',
attributes: options.attributes || {},
events: [],
ended: false,
ended: false
}
return span
@@ -257,6 +255,7 @@ export function startSpan(options: CreateSpanOptions): Span {
if (context.currentSpan) {
context.spanStack.push(context.currentSpan)
}
context.currentSpan = span
return span
@@ -277,7 +276,7 @@ export function endSpan(span: Span, status?: SpanStatus): void {
// Pop span from stack in context
const context = getCurrentContext()
if (context && context.currentSpan === span) {
if (context?.currentSpan === span) {
context.currentSpan = context.spanStack.pop()
}
}
@@ -293,7 +292,7 @@ export function addSpanEvent(span: Span, name: string, attributes?: Record<strin
span.events.push({
name,
timestamp: Date.now(),
attributes,
attributes
})
}
@@ -326,7 +325,7 @@ export function setSpanError(span: Span, error: Error): void {
addSpanEvent(span, 'exception', {
'exception.type': error.name,
'exception.message': error.message,
'exception.message': error.message
})
}
@@ -354,11 +353,11 @@ export function traced(name?: string) {
if (result instanceof Promise) {
return result
.then((value) => {
.then(value => {
endSpan(span, 'ok')
return value
})
.catch((error) => {
.catch(error => {
setSpanError(span, error as Error)
endSpan(span, 'error')
throw error
@@ -381,10 +380,7 @@ export function traced(name?: string) {
/**
* Wrapper for tracing a function
*/
export function traceFunction<T extends (...args: unknown[]) => unknown>(
name: string,
fn: T
): T {
export function traceFunction<T extends(...args: unknown[]) => unknown>(name: string, fn: T): T {
return function (this: unknown, ...args: Parameters<T>): ReturnType<T> {
const span = startSpan({ name })
@@ -393,11 +389,11 @@ export function traceFunction<T extends (...args: unknown[]) => unknown>(
if (result instanceof Promise) {
return result
.then((value) => {
.then(value => {
endSpan(span, 'ok')
return value
})
.catch((error) => {
.catch(error => {
setSpanError(span, error as Error)
endSpan(span, 'error')
throw error
@@ -438,11 +434,7 @@ export async function withSpan<T>(
/**
* Execute sync operation with automatic span
*/
export function withSpanSync<T>(
name: string,
operation: (span: Span) => T,
attributes?: Record<string, unknown>
): T {
export function withSpanSync<T>(name: string, operation: (span: Span) => T, attributes?: Record<string, unknown>): T {
const span = startSpan({ name, attributes })
try {
@@ -504,7 +496,7 @@ export const TRACE_HEADERS = {
SPAN_ID: 'x-span-id',
PARENT_SPAN_ID: 'x-parent-span-id',
CORRELATION_ID: 'x-correlation-id',
BAGGAGE: 'baggage',
BAGGAGE: 'baggage'
} as const
/**
@@ -578,7 +570,7 @@ export function exportContext(context: TraceContext): string {
return JSON.stringify({
traceIds: context.traceIds,
baggage: context.baggage,
metadata: context.metadata,
metadata: context.metadata
})
}
@@ -593,7 +585,7 @@ export function importContext(serialized: string): CreateContextOptions {
parentSpanId: data.traceIds?.spanId,
correlationId: data.traceIds?.correlationId,
baggage: data.baggage,
metadata: data.metadata,
metadata: data.metadata
}
} catch {
return {}
@@ -638,8 +630,9 @@ export function createPrecisionTimer(): PrecisionTimer {
finalDuration = Number(process.hrtime.bigint() - start) / 1_000_000
stopped = true
}
return finalDuration
},
}
}
}
@@ -656,5 +649,5 @@ export default {
withSpanSync,
injectTraceHeaders,
extractTraceHeaders,
createPrecisionTimer,
createPrecisionTimer
}
+25 -43
View File
@@ -14,14 +14,10 @@
* @see https://github.com/WhiskeySockets/Baileys/pull/2294
*/
import { metrics } from './prometheus-metrics.js'
import type { ILogger } from './logger.js'
import type { BinaryNode } from '../WABinary/types.js'
import {
CircuitBreaker,
CircuitOpenError,
createConnectionCircuitBreaker
} from './circuit-breaker.js'
import { CircuitBreaker, CircuitOpenError, createConnectionCircuitBreaker } from './circuit-breaker.js'
import type { ILogger } from './logger.js'
import { metrics } from './prometheus-metrics.js'
// ============================================
// Time Constants (Enterprise-grade)
@@ -41,7 +37,7 @@ const TimeMs = {
/** One day in milliseconds */
Day: 86_400_000,
/** One week in milliseconds */
Week: 604_800_000,
Week: 604_800_000
} as const
/**
@@ -133,11 +129,13 @@ export class UnifiedSessionManager {
constructor(options: UnifiedSessionOptions = {}) {
this.options = {
enabled: options.enabled ?? true,
logger: options.logger ?? console as unknown as ILogger,
logger: options.logger ?? (console as unknown as ILogger),
enableCircuitBreaker: options.enableCircuitBreaker ?? true,
sendNode: options.sendNode ?? (async () => {
throw new Error('sendNode function not configured')
})
sendNode:
options.sendNode ??
(async () => {
throw new Error('sendNode function not configured')
})
}
// Initialize circuit breaker if enabled
@@ -177,15 +175,10 @@ export class UnifiedSessionManager {
return
}
const serverTimeNum = typeof serverTime === 'string'
? parseInt(serverTime, 10)
: serverTime
const serverTimeNum = typeof serverTime === 'string' ? parseInt(serverTime, 10) : serverTime
if (isNaN(serverTimeNum) || serverTimeNum <= 0) {
this.options.logger.debug?.(
{ serverTime },
'Invalid server time received, ignoring'
)
this.options.logger.debug?.({ serverTime }, 'Invalid server time received, ignoring')
return
}
@@ -199,10 +192,7 @@ export class UnifiedSessionManager {
const oldOffset = this.state.serverTimeOffset
this.state.serverTimeOffset = newOffset
this.options.logger.debug?.(
{ oldOffset, newOffset, serverTime: serverTimeNum },
'Server time offset updated'
)
this.options.logger.debug?.({ oldOffset, newOffset, serverTime: serverTimeNum }, 'Server time offset updated')
// Record metric
metrics.socketEvents?.inc({ event: 'server_time_sync' })
@@ -268,10 +258,12 @@ export class UnifiedSessionManager {
const node: BinaryNode = {
tag: 'ib',
attrs: {},
content: [{
tag: 'unified_session',
attrs: { id: sessionId }
}]
content: [
{
tag: 'unified_session',
attrs: { id: sessionId }
}
]
}
const sendOperation = async (): Promise<void> => {
@@ -303,15 +295,9 @@ export class UnifiedSessionManager {
const errorMessage = error instanceof Error ? error.message : String(error)
if (error instanceof CircuitOpenError) {
this.options.logger.warn?.(
{ trigger, circuitState: error.state },
'Unified session blocked by circuit breaker'
)
this.options.logger.warn?.({ trigger, circuitState: error.state }, 'Unified session blocked by circuit breaker')
} else {
this.options.logger.warn?.(
{ trigger, error: errorMessage },
'Failed to send unified session telemetry'
)
this.options.logger.warn?.({ trigger, error: errorMessage }, 'Failed to send unified session telemetry')
}
// Record failure metric
@@ -371,9 +357,7 @@ export class UnifiedSessionManager {
* })
* ```
*/
export function createUnifiedSessionManager(
options: UnifiedSessionOptions = {}
): UnifiedSessionManager {
export function createUnifiedSessionManager(options: UnifiedSessionOptions = {}): UnifiedSessionManager {
return new UnifiedSessionManager(options)
}
@@ -394,11 +378,8 @@ export function extractServerTime(node: BinaryNode): number | undefined {
return undefined
}
const parsed = typeof timeAttr === 'string'
? parseInt(timeAttr, 10)
: typeof timeAttr === 'number'
? timeAttr
: undefined
const parsed =
typeof timeAttr === 'string' ? parseInt(timeAttr, 10) : typeof timeAttr === 'number' ? timeAttr : undefined
if (parsed === undefined || isNaN(parsed) || parsed <= 0) {
return undefined
@@ -418,6 +399,7 @@ export function shouldEnableUnifiedSession(): boolean {
if (envValue !== undefined) {
return envValue.toLowerCase() === 'true' || envValue === '1'
}
// Default: enabled
return true
}
+11 -30
View File
@@ -1,7 +1,7 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { fetchLatestWaWebVersion } from './generics'
import type { WAVersion } from '../Types'
import { fetchLatestWaWebVersion } from './generics'
/**
* Version cache entry stored in file
@@ -86,11 +86,7 @@ async function readCacheFile(filePath: string): Promise<VersionCacheEntry | null
/**
* Writes the cache to file
*/
async function writeCacheFile(
filePath: string,
entry: VersionCacheEntry,
logger?: VersionCacheLogger
): Promise<void> {
async function writeCacheFile(filePath: string, entry: VersionCacheEntry, logger?: VersionCacheLogger): Promise<void> {
try {
await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8')
} catch (error) {
@@ -111,10 +107,7 @@ function isCacheValid(entry: VersionCacheEntry | null, ttlMs: number): boolean {
/**
* Fetches version with deduplication (prevents 150 parallel requests)
*/
async function fetchVersionOnce(
cacheFilePath: string,
logger?: VersionCacheLogger
): Promise<VersionCacheEntry> {
async function fetchVersionOnce(cacheFilePath: string, logger?: VersionCacheLogger): Promise<VersionCacheEntry> {
logger?.info({}, 'Fetching WhatsApp Web version (shared for all connections)...')
const result = await fetchLatestWaWebVersion()
@@ -131,10 +124,7 @@ async function fetchVersionOnce(
// Persist to file (async, don't wait)
writeCacheFile(cacheFilePath, entry, logger).catch(() => {})
logger?.info(
{ version: entry.version, source: entry.source },
'Version fetched and cached'
)
logger?.info({ version: entry.version, source: entry.source }, 'Version fetched and cached')
return entry
}
@@ -158,11 +148,7 @@ async function fetchVersionOnce(
export async function getCachedVersion(
config: VersionCacheConfig = {}
): Promise<{ version: WAVersion; fromCache: boolean; age: number; source: 'online' | 'fallback' | 'memory' | 'file' }> {
const {
cacheTtlMs = DEFAULT_CACHE_TTL_MS,
cacheFilePath = DEFAULT_CACHE_FILE,
logger
} = config
const { cacheTtlMs = DEFAULT_CACHE_TTL_MS, cacheFilePath = DEFAULT_CACHE_FILE, logger } = config
// 1. Check memory cache first (fastest)
if (isCacheValid(memoryCache, cacheTtlMs) && memoryCache) {
@@ -183,8 +169,9 @@ export async function getCachedVersion(
// 3. Need to fetch - but deduplicate concurrent requests
// If 150 connections start at once, only 1 fetch happens
if (!fetchInProgress) {
fetchInProgress = fetchVersionOnce(cacheFilePath, logger)
.finally(() => { fetchInProgress = null })
fetchInProgress = fetchVersionOnce(cacheFilePath, logger).finally(() => {
fetchInProgress = null
})
}
const entry = await fetchInProgress
@@ -195,9 +182,7 @@ export async function getCachedVersion(
* Clears the version cache (memory and file).
* Also cancels any in-progress fetch to prevent it from restoring the cache.
*/
export async function clearVersionCache(
cacheFilePath: string = DEFAULT_CACHE_FILE
): Promise<void> {
export async function clearVersionCache(cacheFilePath: string = DEFAULT_CACHE_FILE): Promise<void> {
// Wait for any in-progress fetch to complete before clearing
// This prevents the fetch from restoring the cache after we clear it
if (fetchInProgress) {
@@ -224,9 +209,7 @@ export async function clearVersionCache(
*
* @returns Object with version, success status, and source
*/
export async function refreshVersionCache(
config: VersionCacheConfig = {}
): Promise<RefreshVersionResult> {
export async function refreshVersionCache(config: VersionCacheConfig = {}): Promise<RefreshVersionResult> {
const { cacheFilePath = DEFAULT_CACHE_FILE, logger } = config
// Wait for any existing fetch to complete first (deduplication)
@@ -260,9 +243,7 @@ export async function refreshVersionCache(
/**
* Gets cache status information
*/
export function getVersionCacheStatus(
cacheTtlMs: number = DEFAULT_CACHE_TTL_MS
): {
export function getVersionCacheStatus(cacheTtlMs: number = DEFAULT_CACHE_TTL_MS): {
hasCache: boolean
version: WAVersion | null
age: number | null
+3 -3
View File
@@ -15,8 +15,7 @@ export { _bridgeReady as wasmBridgeReady }
function getBridge(): WasmBridgeModule {
if (!_bridge) {
throw new Error(
'whatsapp-rust-bridge not yet loaded. ' +
'Ensure async operations have started before calling crypto functions.'
'whatsapp-rust-bridge not yet loaded. ' + 'Ensure async operations have started before calling crypto functions.'
)
}
@@ -27,7 +26,8 @@ export const hkdf: WasmBridgeModule['hkdf'] = (...args) => getBridge().hkdf(...a
export const md5: WasmBridgeModule['md5'] = (...args) => getBridge().md5(...args)
export const expandAppStateKeys: WasmBridgeModule['expandAppStateKeys'] = (...args) => getBridge().expandAppStateKeys(...args)
export const expandAppStateKeys: WasmBridgeModule['expandAppStateKeys'] = (...args) =>
getBridge().expandAppStateKeys(...args)
let _ltHash: InstanceType<WasmBridgeModule['LTHashAntiTampering']> | undefined