feat(telemetry): add unified_session telemetry to reduce detection

Implements WhatsApp's unified_session telemetry feature to reduce
detection of unofficial clients. This is an enterprise-grade implementation
inspired by whatsmeow PR #1057 and Baileys PR #2294.

Features:
- UnifiedSessionManager class with circuit breaker protection
- Server time synchronization for accurate session IDs
- Rate limiting to prevent spam (1 minute between sends)
- Prometheus metrics integration (unified_session_sent, errors)
- Structured logging for debugging
- Configurable via SocketConfig.enableUnifiedSession
- Environment variable support (BAILEYS_UNIFIED_SESSION_ENABLED)

Trigger points (matching official WhatsApp Web):
- After successful login (CB:success)
- After successful pairing (CB:iq,,pair-success)
- When sending 'available' presence

Implementation details:
- Session ID algorithm: (now + serverOffset + 3days) % 7days
- Time constants exported from Defaults/index.ts
- Full test coverage (31 tests)

References:
- https://github.com/tulir/whatsmeow/pull/1057
- https://github.com/WhiskeySockets/Baileys/pull/2294
- https://github.com/tulir/whatsmeow/issues/810
This commit is contained in:
Claude
2026-01-24 18:56:08 +00:00
parent a55055e55e
commit 9f17567951
7 changed files with 948 additions and 3 deletions
+71 -1
View File
@@ -60,6 +60,12 @@ import {
incrementActiveConnections,
decrementActiveConnections
} from '../Utils/prometheus-metrics'
import {
createUnifiedSessionManager,
extractServerTime,
shouldEnableUnifiedSession,
type UnifiedSessionManager
} from '../Utils/unified-session'
import { BinaryInfo } from '../WAM/BinaryInfo.js'
import { USyncQuery, USyncUser } from '../WAUSync/'
import { WebSocketClient } from './Client'
@@ -87,7 +93,8 @@ export const makeSocket = (config: SocketConfig) => {
enableCircuitBreaker = true,
queryCircuitBreaker: queryCircuitBreakerConfig,
connectionCircuitBreaker: connectionCircuitBreakerConfig,
preKeyCircuitBreaker: preKeyCircuitBreakerConfig
preKeyCircuitBreaker: preKeyCircuitBreakerConfig,
enableUnifiedSession = shouldEnableUnifiedSession()
} = config
// Initialize circuit breakers if enabled
@@ -141,6 +148,19 @@ export const makeSocket = (config: SocketConfig) => {
logger.info('Circuit breakers initialized for socket operations')
}
// Initialize Unified Session Manager for telemetry
// Note: sendNode will be set after it's defined below
let unifiedSessionManager: UnifiedSessionManager | undefined
if (enableUnifiedSession) {
unifiedSessionManager = createUnifiedSessionManager({
enabled: true,
logger,
enableCircuitBreaker
// sendNode will be configured after sendNode function is defined
})
logger.info('Unified session manager initialized')
}
const publicWAMBuffer = new BinaryInfo()
const uqTagId = generateMdTagPrefix()
@@ -220,6 +240,28 @@ export const makeSocket = (config: SocketConfig) => {
return sendRawMessage(buff)
}
// Configure unified session manager with sendNode now that it's defined
if (unifiedSessionManager) {
// Create a wrapper that matches the expected signature
const sendNodeForSession = async (node: BinaryNode): Promise<void> => {
await sendNode(node)
}
// Recreate with proper sendNode
unifiedSessionManager = createUnifiedSessionManager({
enabled: true,
logger,
enableCircuitBreaker,
sendNode: sendNodeForSession
})
}
/** Send unified_session telemetry */
const sendUnifiedSession = async (trigger: 'login' | 'pairing' | 'presence' | 'manual' = 'manual'): Promise<void> => {
if (unifiedSessionManager) {
await unifiedSessionManager.send(trigger)
}
}
/**
* Wait for a message with a certain tag to be received
* @param msgId the message tag to await
@@ -783,6 +825,9 @@ export const makeSocket = (config: SocketConfig) => {
connectionCircuitBreaker?.destroy()
preKeyCircuitBreaker?.destroy()
// Clean up unified session manager
unifiedSessionManager?.destroy()
ws.removeAllListeners('close')
ws.removeAllListeners('open')
ws.removeAllListeners('message')
@@ -1061,6 +1106,11 @@ export const makeSocket = (config: SocketConfig) => {
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
await sendNode(reply)
// Send unified_session telemetry on successful pairing
sendUnifiedSession('pairing').catch(err => {
logger.debug({ err }, 'Failed to send unified_session on pairing')
})
} catch (error: any) {
logger.info({ trace: error.stack }, 'error in pairing')
void end(error)
@@ -1093,6 +1143,17 @@ export const makeSocket = (config: SocketConfig) => {
recordConnectionAttempt('success')
incrementActiveConnections()
// Update server time offset from success node
const serverTime = extractServerTime(node)
if (serverTime) {
unifiedSessionManager?.updateServerTimeOffset(serverTime)
}
// Send unified_session telemetry on successful login
sendUnifiedSession('login').catch(err => {
logger.debug({ err }, 'Failed to send unified_session on login')
})
if (node.attrs.lid && authState.creds.me?.id) {
const myLID = node.attrs.lid
process.nextTick(async () => {
@@ -1247,6 +1308,15 @@ export const makeSocket = (config: SocketConfig) => {
connectionCircuitBreaker?.reset()
preKeyCircuitBreaker?.reset()
logger.info('All circuit breakers reset to closed state')
},
// Unified Session Telemetry
/** Send unified_session telemetry manually */
sendUnifiedSession,
/** Get unified session manager state (for debugging) */
getUnifiedSessionState: () => unifiedSessionManager?.getState(),
/** Update server time offset (call when receiving server timestamps) */
updateServerTimeOffset: (serverTime: string | number) => {
unifiedSessionManager?.updateServerTimeOffset(serverTime)
}
}
}