Compare commits

..

7 Commits

Author SHA1 Message Date
Renato Alcara 27b942f03c feat: change default browser to Android (SMB_ANDROID)
Default is now Browsers.android('14') matching upstream PR #2201.
Pair code auto-detects Android and falls back to Chrome (socket.ts).
Set BAILEYS_BROWSER=chrome to force Chrome/macOS if needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:43:24 -03:00
Renato Alcara 0eae108ba4 fix: move connection log to CB:success and rename type to platformType
Move the presentation log from pre-handshake (validateConnection) to
CB:success where the connection is actually confirmed. Rename ambiguous
'type' label to 'platformType' matching DeviceProps.PlatformType.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:42:40 -03:00
Renato Alcara cb80febba5 feat: add connection and pair code presentation logs
Single-line logs showing how the lib presents to WhatsApp:
- 📱/🖥️ Connection: phone number, platform, device, type
- 🔗 Pair code: companion platform, android override status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:22:12 -03:00
Renato Alcara 31ac5aeb11 feat: add Android browser preset for companion device registration (#248)
* feat: add Android browser preset for companion device registration

Add Browsers.android(apiLevel) preset that identifies as an Android
companion device (SMB_ANDROID platform, ANDROID_PHONE device type).
Validated against Frida captures of real WhatsApp Android protocol.


* feat: support BAILEYS_BROWSER env var for browser selection

Allows selecting Android companion mode via environment variable
without code changes. Set BAILEYS_BROWSER=android (or android:15
for a specific API level) to register as an Android device.

* fix: auto-detect Android browser in pair code and fall back to Chrome

Pair code companion registration fails with Android browser because the
WA server rejects the ANDROID_PHONE companion_platform_id over the web
Noise protocol. When Android is detected, requestPairingCode() now
transparently uses Chrome/macOS platform values for the pair code stanza
while QR code pairing continues to work as Android (SMB_ANDROID).

* docs: add android to isValidBrowserPreset JSDoc
2026-03-02 12:02:08 -03:00
Renato Alcara 45885c7a01 chore: update WhatsApp Web version to v2.3000.1034300341 (#250)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 06:14:04 -03:00
github-actions[bot] f1b43e26a1 chore: update proto/version to v2.3000.1034293476 (#249)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-02 00:45:19 -03:00
Renato Alcara a3dd21c9d7 fix: break infinite Bad MAC + session recreation loop (#247)
* fix: break infinite Bad MAC + session recreation loop

Remove aggressive session cleanup from the decryption hot path that caused
cascading failures when multiple messages from the same contact arrived
simultaneously. The Signal Protocol naturally recovers via retry+pkmsg flow.

Changes:
- Remove cleanupCorruptedSession() from per-message error handler (hot path)
- Move session cleanup to retry-exhausted handler as safety net
- Add per-JID retry request deduplication (5s window)
- Add 10s cooldown on MAC error session recreation
- Export cleanupCorruptedSession/getDecryptionJid for use in messages-recv

Tested: corrupted session with fake keys → Bad MAC → retry receipt →
pkmsg recovery → all 7 messages delivered, zero infinite loop.


* fix: address PR review — lint, dedup scope, and cleanup targeting

- Remove unused `autoCleanCorrupted` param from decryptMessageNode (lint fix)
- Scope retry dedup by participant JID in group chats (not group JID)
- Move dedup registration after early-return checks to avoid blocking
  subsequent messages when max retries reached
- Use participant JID for session cleanup in groups (Signal sessions
  are per-participant, not per-group)
2026-03-01 20:59:47 -03:00
7 changed files with 77 additions and 17 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1034274421
/// WhatsApp Version: 2.3000.1034293476
message ADVDeviceIdentity {
optional uint32 rawId = 1;
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1034279434]}
{"version":[2,3000,1034300341]}
+24 -1
View File
@@ -56,10 +56,33 @@ export const PROCESSABLE_HISTORY_TYPES = [
// 6 hours in milliseconds
const SIX_HOURS_MS = 6 * 60 * 60 * 1000
/**
* Resolves the default browser tuple from the BAILEYS_BROWSER env var.
* Default: Android companion (SMB_ANDROID) — matches upstream PR #2201.
* Pair code auto-detects Android and falls back to Chrome in socket.ts.
*
* unset / 'android' → Browsers.android('14')
* 'android:15' → Browsers.android('15')
* 'chrome' / 'macos' → Browsers.macOS('Chrome')
*/
const resolveDefaultBrowser = (): [string, string, string] => {
const env = process.env.BAILEYS_BROWSER?.trim().toLowerCase()
if (env === 'chrome' || env === 'macos') {
return Browsers.macOS('Chrome')
}
if (env?.startsWith('android:')) {
const apiLevel = env.split(':')[1] || '14'
return Browsers.android(apiLevel)
}
return Browsers.android('14')
}
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
version: version as WAVersion,
versionCheckIntervalMs: SIX_HOURS_MS,
browser: Browsers.macOS('Chrome'),
browser: resolveDefaultBrowser(),
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 15_000,
+16 -6
View File
@@ -38,7 +38,7 @@ import {
signedKeyPair,
xmppSignedPreKey
} from '../Utils'
import { getPlatformId } from '../Utils/browser-utils'
import { getPlatformId, isAndroidBrowser } from '../Utils/browser-utils'
import {
CircuitBreaker,
CircuitOpenError,
@@ -634,8 +634,6 @@ export const makeSocket = (config: SocketConfig) => {
}
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
logger.info({ browser, helloMsg }, 'connected to WA')
const init = proto.HandshakeMessage.encode(helloMsg).finish()
const result = await awaitNextMessage<Uint8Array>(init)
@@ -1352,6 +1350,16 @@ export const makeSocket = (config: SocketConfig) => {
id: jidEncode(phoneNumber, 's.whatsapp.net'),
name: '~'
}
// Pair code with Android browser doesn't work (WA server rejects the
// companion registration). When Android is detected, fall back to the
// Chrome/macOS platform values that are known to work.
const isAndroid = isAndroidBrowser(browser)
const pairPlatformId = isAndroid ? getPlatformId('Chrome') : getPlatformId(browser[1])
const pairPlatformDisplay = isAndroid ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})`
logger.info(`\uD83D\uDD17 Pair code requested | companion: ${pairPlatformDisplay} | ${isAndroid ? 'android override \u2192 Chrome' : 'native platform'}`)
ev.emit('creds.update', authState.creds)
await sendNode({
tag: 'iq',
@@ -1384,12 +1392,12 @@ export const makeSocket = (config: SocketConfig) => {
{
tag: 'companion_platform_id',
attrs: {},
content: getPlatformId(browser[1])
content: pairPlatformId
},
{
tag: 'companion_platform_display',
attrs: {},
content: `${browser[1]} (${browser[0]})`
content: pairPlatformDisplay
},
{
tag: 'link_code_pairing_nonce',
@@ -1519,7 +1527,9 @@ export const makeSocket = (config: SocketConfig) => {
})
// login complete
ws.on('CB:success', async (node: BinaryNode) => {
logger.info('opened connection to WA')
const isAndroid = isAndroidBrowser(browser)
const phoneId = authState.creds.me?.id?.split(':')[0]?.split('@')[0] || 'new session'
logger.info(`${isAndroid ? '\uD83D\uDCF1' : '\uD83D\uDDA5\uFE0F'} Connected to WA | ${phoneId} | platform: ${isAndroid ? 'SMB_ANDROID' : 'MACOS'} | device: ${isAndroid ? 'Android' : 'Desktop'} | platformType: ${isAndroid ? 'ANDROID_PHONE' : 'CHROME'}`)
clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try
ev.emit('creds.update', { me: { ...authState.creds.me!, lid: node.attrs.lid } })
+1
View File
@@ -23,6 +23,7 @@ export type BrowsersMap = {
baileys(browser: string): [string, string, string]
windows(browser: string): [string, string, string]
appropriate(browser: string): [string, string, string]
android(apiLevel: string): [string, string, string]
}
export enum DisconnectReason {
+20 -3
View File
@@ -94,7 +94,13 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
}
}
return new Map(entries)
const map = new Map(entries)
// ANDROID → ANDROID_PHONE alias (DeviceProps.PlatformType has ANDROID_PHONE but not ANDROID)
if (!map.has('ANDROID') && map.has('ANDROID_PHONE')) {
map.set('ANDROID', map.get('ANDROID_PHONE')!)
}
return map
})()
// ============================================================
@@ -331,7 +337,8 @@ 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()],
android: (apiLevel: string): [string, string, string] => [apiLevel, 'Android', '']
} as const
/**
@@ -378,7 +385,7 @@ export const getPlatformId = (browser: unknown): string => {
* properties like 'toString' or 'constructor'.
*
* @param value - Value to check
* @returns True if value is a valid browser preset key ('ubuntu', 'macOS', 'windows', 'baileys', 'appropriate')
* @returns True if value is a valid browser preset key ('ubuntu', 'macOS', 'windows', 'baileys', 'appropriate', 'android')
*
* @example
* isValidBrowserPreset('ubuntu') // true
@@ -391,6 +398,16 @@ export const getPlatformId = (browser: unknown): string => {
* const config = Browsers[userInput]('MyApp')
* }
*/
/**
* Checks if the browser tuple represents an Android companion device.
*
* @param browser - Browser tuple [os, platform, version]
* @returns True if platform is 'Android' (case-insensitive)
*/
export const isAndroidBrowser = (browser: [string, string, string]): boolean => {
return browser[1]?.toUpperCase() === 'ANDROID'
}
export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => {
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(Browsers, value)
}
+14 -5
View File
@@ -9,24 +9,27 @@ import {
} from '../Defaults'
import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types'
import { type BinaryNode, getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary'
import { isAndroidBrowser } from './browser-utils'
import { Curve, hmacSign } from './crypto'
import { encodeBigEndian } from './generics'
import { createSignalIdentity } from './signal'
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
const isAndroid = isAndroidBrowser(config.browser)
return {
appVersion: {
primary: config.version[0],
secondary: config.version[1],
tertiary: config.version[2]
},
platform: proto.ClientPayload.UserAgent.Platform.MACOS,
platform: isAndroid
? proto.ClientPayload.UserAgent.Platform.SMB_ANDROID
: proto.ClientPayload.UserAgent.Platform.MACOS,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
osVersion: '0.1',
device: 'Desktop',
osVersion: isAndroid ? config.browser[0] : '0.1',
device: isAndroid ? 'Android' : 'Desktop',
osBuildNumber: '0.1',
localeLanguageIso6391: 'en',
mnc: '000',
mcc: '000',
localeCountryIso31661Alpha2: config.countryCode
@@ -58,7 +61,9 @@ const getClientPayload = (config: SocketConfig) => {
userAgent: getUserAgent(config)
}
payload.webInfo = getWebInfo(config)
if (!isAndroidBrowser(config.browser)) {
payload.webInfo = getWebInfo(config)
}
return payload
}
@@ -84,6 +89,10 @@ export const generateLoginNode = (userJid: string, config: SocketConfig): proto.
const getPlatformType = (platform: string): proto.DeviceProps.PlatformType => {
const platformType = platform.toUpperCase()
if (platformType === 'ANDROID') {
return proto.DeviceProps.PlatformType.ANDROID_PHONE
}
return (
proto.DeviceProps.PlatformType[platformType as keyof typeof proto.DeviceProps.PlatformType] ||
proto.DeviceProps.PlatformType.CHROME