refactor(browser-utils): improve robustness and type safety

refactor(browser-utils): improve robustness and type safety
This commit is contained in:
Renato Alcara
2026-01-30 12:34:19 -03:00
committed by GitHub
2 changed files with 658 additions and 16 deletions
+379 -16
View File
@@ -1,31 +1,394 @@
import { platform, release } from 'os'
import { readFileSync, existsSync } from 'fs'
import { proto } from '../../WAProto/index.js'
import type { BrowsersMap } from '../Types'
const PLATFORM_MAP = {
// ============================================================
// Constants
// ============================================================
/**
* Default platform name when OS detection fails or returns unsupported value
*/
const DEFAULT_PLATFORM_NAME = 'Ubuntu' as const
/**
* Default platform ID (Chrome = 1) for unknown browser mappings
* @see proto.DeviceProps.PlatformType
*/
const DEFAULT_PLATFORM_ID = '1' as const
/**
* Default OS version fallback when detection fails
*/
const DEFAULT_OS_VERSION = '1.0.0' as const
/**
* Fallback versions used when automatic detection fails.
* These should be updated periodically to reflect current OS versions.
* Last updated: 2025-01
*/
const FALLBACK_VERSIONS = {
ubuntu: '24.04.1',
macOS: '15.2',
windows: '10.0.26100',
baileys: '6.5.0',
} as const
/**
* Maps Darwin kernel versions to macOS marketing versions.
* Darwin 24.x = macOS 15.x (Sequoia)
* Darwin 23.x = macOS 14.x (Sonoma)
* Darwin 22.x = macOS 13.x (Ventura)
* Darwin 21.x = macOS 12.x (Monterey)
* Darwin 20.x = macOS 11.x (Big Sur)
*/
const DARWIN_TO_MACOS: Readonly<Record<number, number>> = {
24: 15, // Sequoia
23: 14, // Sonoma
22: 13, // Ventura
21: 12, // Monterey
20: 11, // Big Sur
19: 10, // Catalina (10.15)
} as const
/**
* Maps Node.js platform identifiers to WhatsApp-recognized platform names.
* Values of `undefined` will fall back to DEFAULT_PLATFORM_NAME.
*/
const PLATFORM_MAP: Readonly<Record<NodeJS.Platform, string | undefined>> = {
aix: 'AIX',
darwin: 'Mac OS',
win32: 'Windows',
android: 'Android',
cygwin: undefined,
darwin: 'Mac OS',
freebsd: 'FreeBSD',
haiku: undefined,
linux: undefined,
netbsd: undefined,
openbsd: 'OpenBSD',
sunos: 'Solaris',
linux: undefined,
haiku: undefined,
cygwin: undefined,
netbsd: undefined
win32: 'Windows',
} as const
// ============================================================
// Platform Type Resolution
// ============================================================
/**
* Pre-computed map of browser names to platform IDs.
* Built once at module load to avoid repeated proto access.
*
* Note: Protobuf enums have bidirectional mappings (name→value and value→name).
* We filter to only include string keys with numeric values.
*/
const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
const platformType = proto.DeviceProps?.PlatformType
if (!platformType || typeof platformType !== 'object') {
return new Map<string, string>()
}
const entries: Array<[string, string]> = []
for (const [key, value] of Object.entries(platformType)) {
if (typeof value === 'number' && typeof key === 'string' && !/^\d+$/.test(key)) {
entries.push([key.toUpperCase(), value.toString()])
}
}
return new Map(entries)
})()
// ============================================================
// Version Detection Functions
// ============================================================
/**
* Detects the Linux distribution version by reading /etc/os-release.
* This file is standard on most modern Linux distributions.
*
* @returns The VERSION_ID from os-release, or fallback version
*
* @example
* // On Ubuntu 24.04:
* getLinuxVersion() // Returns '24.04' or '24.04.1'
*/
const getLinuxVersion = (): string => {
try {
const osReleasePaths = ['/etc/os-release', '/usr/lib/os-release']
for (const filePath of osReleasePaths) {
if (existsSync(filePath)) {
const content = readFileSync(filePath, 'utf-8')
// Try VERSION_ID first (e.g., "24.04")
const versionIdMatch = content.match(/^VERSION_ID\s*=\s*"?([^"\n]+)"?/m)
if (versionIdMatch?.[1]) {
return versionIdMatch[1]
}
// Fallback to VERSION (e.g., "24.04.1 LTS (Noble Numbat)")
const versionMatch = content.match(/^VERSION\s*=\s*"?([0-9][0-9.]*)/m)
if (versionMatch?.[1]) {
return versionMatch[1]
}
}
}
} catch {
// Silently fail and use fallback
}
return FALLBACK_VERSIONS.ubuntu
}
/**
* Converts Darwin kernel version to macOS marketing version.
* Darwin versions map to macOS versions with an offset.
*
* @returns macOS version string (e.g., '15.2')
*
* @example
* // On macOS Sequoia with Darwin 24.2.0:
* getMacOSVersion() // Returns '15.2'
*/
const getMacOSVersion = (): string => {
try {
const darwinVersion = release() // e.g., '24.2.0'
const parts = darwinVersion.split('.')
const majorVersionStr = parts[0]
const minorVersionStr = parts[1]
if (!majorVersionStr) {
return FALLBACK_VERSIONS.macOS
}
const majorVersion = parseInt(majorVersionStr, 10)
if (isNaN(majorVersion)) {
return FALLBACK_VERSIONS.macOS
}
const minorVersion = minorVersionStr || '0'
const macOSMajor = DARWIN_TO_MACOS[majorVersion]
if (macOSMajor === undefined) {
// For newer Darwin versions, estimate macOS version
// Darwin 24 = macOS 15, so offset is 9
const estimatedMajor = majorVersion >= 20 ? majorVersion - 9 : 10
return `${estimatedMajor}.${minorVersion}`
}
// Special case: macOS 10.x (Catalina and earlier)
if (macOSMajor === 10) {
return `10.15.${minorVersion}`
}
return `${macOSMajor}.${minorVersion}`
} catch {
return FALLBACK_VERSIONS.macOS
}
}
/**
* Gets Windows version directly from os.release().
* Windows correctly reports version (e.g., '10.0.22631').
*
* @returns Windows version string
*/
const getWindowsVersion = (): string => {
try {
const version = release()
// Validate it looks like a Windows version (X.X.XXXXX)
if (/^\d+\.\d+\.\d+$/.test(version)) {
return version
}
} catch {
// Silently fail
}
return FALLBACK_VERSIONS.windows
}
/**
* Detects the current OS version automatically based on the platform.
* Uses platform-specific methods for accurate version detection.
*
* @returns Object containing detected versions for all platforms
*/
const detectOSVersions = (): Readonly<{
ubuntu: string
macOS: string
windows: string
baileys: string
}> => {
const currentPlatform = (() => {
try {
return platform()
} catch {
return 'linux' as NodeJS.Platform
}
})()
// Detect versions based on current platform
// For non-matching platforms, use fallback values
return {
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,
}
}
/**
* Cached OS versions, detected once at module load.
* This ensures consistent versions throughout the application lifecycle.
*/
const OS_VERSIONS = detectOSVersions()
// ============================================================
// Helper Functions
// ============================================================
/**
* Resolves the current platform name for WhatsApp device identification.
* Uses the OS platform detection with a fallback to Ubuntu.
*
* @returns Platform name string (never undefined)
*/
const getPlatformName = (): string => {
try {
const currentPlatform = platform()
return PLATFORM_MAP[currentPlatform] ?? DEFAULT_PLATFORM_NAME
} catch {
return DEFAULT_PLATFORM_NAME
}
}
/**
* Normalizes a browser identifier for platform type lookup.
*
* @param browser - The browser identifier to normalize (accepts any type for runtime safety)
* @returns Uppercase trimmed string, or null if input is invalid
*/
const normalizeBrowserKey = (browser: unknown): string | null => {
if (typeof browser !== 'string') {
return null
}
const normalized = browser.trim()
return normalized.length > 0 ? normalized.toUpperCase() : null
}
/**
* Gets the appropriate OS version for the current platform.
* Uses automatic detection with fallback to sensible defaults.
*
* @returns OS version string appropriate for the current platform
*/
const getAppropriateVersion = (): string => {
try {
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
}
} catch {
return DEFAULT_OS_VERSION
}
}
// ============================================================
// Exported Constants
// ============================================================
/**
* Browser configuration presets for WhatsApp device registration.
* Each factory returns a tuple of [platform, browser, version].
*
* Versions are automatically detected at module load:
* - Linux: Reads /etc/os-release for distribution version
* - macOS: Converts Darwin kernel version to macOS version
* - Windows: Uses os.release() directly
*
* @example
* // Use Ubuntu preset with auto-detected version
* const config = Browsers.ubuntu('Chrome')
* // Returns: ['Ubuntu', 'Chrome', '24.04.1'] (version detected automatically)
*
* @example
* // Use automatic platform and version detection
* const config = Browsers.appropriate('MyApp')
* // Returns: ['Mac OS', 'MyApp', '15.2'] (on macOS Sequoia)
*/
export const Browsers: BrowsersMap = {
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
macOS: browser => ['Mac OS', browser, '14.4.1'],
baileys: browser => ['Baileys', browser, '6.5.0'],
windows: browser => ['Windows', browser, '10.0.22631'],
/** The appropriate browser based on your OS & release */
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
ubuntu: (browser: string): [string, string, string] => ['Ubuntu', browser, OS_VERSIONS.ubuntu],
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()],
} as const
/**
* Exposed OS versions for debugging and logging purposes.
* These are the versions that will be used by the Browsers presets.
*/
export const detectedOSVersions = OS_VERSIONS
// ============================================================
// Exported Functions
// ============================================================
/**
* Resolves the platform type ID for a given browser name.
* Uses the WhatsApp protocol buffer definitions for mapping.
*
* This function safely handles invalid inputs (null, undefined, non-strings)
* by returning the default Chrome platform ID.
*
* @param browser - Browser identifier (e.g., 'chrome', 'firefox', 'safari').
* Accepts unknown types for runtime safety.
* @returns Platform type ID as string (defaults to '1' for Chrome)
*
* @example
* getPlatformId('chrome') // Returns '1'
* getPlatformId('CHROME') // Returns '1' (case-insensitive)
* getPlatformId('firefox') // Returns platform-specific ID
* getPlatformId('') // Returns '1' (default)
*/
export const getPlatformId = (browser: unknown): string => {
const key = normalizeBrowserKey(browser)
if (key === null) {
return DEFAULT_PLATFORM_ID
}
return BROWSER_TO_PLATFORM_ID.get(key) ?? DEFAULT_PLATFORM_ID
}
export const getPlatformId = (browser: string) => {
const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase() as any]
return platformType ? platformType.toString() : '1' //chrome
/**
* Type guard to check if a value is a valid browser preset key.
* Useful for external validation before using Browsers presets.
*
* Uses Object.prototype.hasOwnProperty to avoid matching inherited
* 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')
*
* @example
* isValidBrowserPreset('ubuntu') // true
* isValidBrowserPreset('macOS') // true
* isValidBrowserPreset('invalid') // false
* isValidBrowserPreset('toString') // false (inherited property)
*
* @example
* if (isValidBrowserPreset(userInput)) {
* const config = Browsers[userInput]('MyApp')
* }
*/
export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => {
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(Browsers, value)
}
+279
View File
@@ -0,0 +1,279 @@
import { Browsers, getPlatformId, isValidBrowserPreset, detectedOSVersions } from '../../Utils/browser-utils'
describe('browser-utils', () => {
describe('Browsers', () => {
describe('ubuntu', () => {
it('should return correct tuple for Ubuntu', () => {
const result = Browsers.ubuntu('Chrome')
expect(result[0]).toBe('Ubuntu')
expect(result[1]).toBe('Chrome')
expect(result[2]).toMatch(/^\d+\.\d+/) // Version format like '24.04' or '24.04.1'
})
it('should handle empty browser string', () => {
const result = Browsers.ubuntu('')
expect(result[0]).toBe('Ubuntu')
expect(result[1]).toBe('')
})
it('should use auto-detected or fallback version', () => {
const result = Browsers.ubuntu('Test')
// Version should be a non-empty string
expect(typeof result[2]).toBe('string')
expect(result[2].length).toBeGreaterThan(0)
})
})
describe('macOS', () => {
it('should return correct tuple for macOS', () => {
const result = Browsers.macOS('Safari')
expect(result[0]).toBe('Mac OS')
expect(result[1]).toBe('Safari')
expect(result[2]).toMatch(/^\d+\.\d+/) // Version format like '15.2'
})
})
describe('windows', () => {
it('should return correct tuple for Windows', () => {
const result = Browsers.windows('Edge')
expect(result[0]).toBe('Windows')
expect(result[1]).toBe('Edge')
expect(result[2]).toMatch(/^\d+\.\d+\.\d+/) // Version format like '10.0.26100'
})
})
describe('baileys', () => {
it('should return correct tuple for Baileys', () => {
const result = Browsers.baileys('Baileys')
expect(result).toEqual(['Baileys', 'Baileys', '6.5.0'])
})
})
describe('appropriate', () => {
it('should return a valid tuple with platform detection', () => {
const result = Browsers.appropriate('MyApp')
expect(result).toHaveLength(3)
expect(typeof result[0]).toBe('string')
expect(result[1]).toBe('MyApp')
expect(typeof result[2]).toBe('string')
})
it('should never return undefined for platform', () => {
const result = Browsers.appropriate('Test')
expect(result[0]).toBeDefined()
expect(result[0]).not.toBe('')
// Should be a valid platform name
expect(['Ubuntu', 'Mac OS', 'Windows', 'AIX', 'Android', 'FreeBSD', 'OpenBSD', 'Solaris']).toContain(result[0])
})
it('should return a valid version string', () => {
const result = Browsers.appropriate('Test')
expect(result[2]).toBeDefined()
expect(typeof result[2]).toBe('string')
expect(result[2].length).toBeGreaterThan(0)
})
})
})
describe('detectedOSVersions', () => {
it('should expose detected versions', () => {
expect(detectedOSVersions).toBeDefined()
expect(typeof detectedOSVersions.ubuntu).toBe('string')
expect(typeof detectedOSVersions.macOS).toBe('string')
expect(typeof detectedOSVersions.windows).toBe('string')
expect(typeof detectedOSVersions.baileys).toBe('string')
})
it('should have valid version formats', () => {
// Ubuntu: X.X or X.X.X
expect(detectedOSVersions.ubuntu).toMatch(/^\d+\.\d+(\.\d+)?$/)
// macOS: X.X or X.X.X
expect(detectedOSVersions.macOS).toMatch(/^\d+\.\d+(\.\d+)?$/)
// Windows: X.X.XXXXX
expect(detectedOSVersions.windows).toMatch(/^\d+\.\d+\.\d+$/)
// Baileys: semver
expect(detectedOSVersions.baileys).toMatch(/^\d+\.\d+\.\d+$/)
})
it('should match Browsers preset versions', () => {
expect(Browsers.ubuntu('Test')[2]).toBe(detectedOSVersions.ubuntu)
expect(Browsers.macOS('Test')[2]).toBe(detectedOSVersions.macOS)
expect(Browsers.windows('Test')[2]).toBe(detectedOSVersions.windows)
expect(Browsers.baileys('Test')[2]).toBe(detectedOSVersions.baileys)
})
})
describe('getPlatformId', () => {
it('should return platform ID for valid browser string', () => {
const result = getPlatformId('chrome')
expect(result).toBe('1')
})
it('should be case-insensitive', () => {
const lowercase = getPlatformId('chrome')
const uppercase = getPlatformId('CHROME')
const mixed = getPlatformId('Chrome')
expect(lowercase).toBe(uppercase)
expect(uppercase).toBe(mixed)
})
it('should return default ID for empty string', () => {
const result = getPlatformId('')
expect(result).toBe('1')
})
it('should return default ID for whitespace-only string', () => {
const result = getPlatformId(' ')
expect(result).toBe('1')
})
it('should return default ID for unknown browser', () => {
const result = getPlatformId('unknown_browser_xyz')
expect(result).toBe('1')
})
it('should handle string with leading/trailing whitespace', () => {
const result = getPlatformId(' chrome ')
expect(result).toBe('1')
})
it('should return default ID for undefined input', () => {
const result = getPlatformId(undefined)
expect(result).toBe('1')
})
it('should return default ID for null input', () => {
const result = getPlatformId(null)
expect(result).toBe('1')
})
it('should return default ID for number input', () => {
const result = getPlatformId(123)
expect(result).toBe('1')
})
it('should return default ID for object input', () => {
const result = getPlatformId({ browser: 'chrome' })
expect(result).toBe('1')
})
it('should return default ID for array input', () => {
const result = getPlatformId(['chrome'])
expect(result).toBe('1')
})
it('should always return a string', () => {
const validResult = getPlatformId('chrome')
const invalidResult = getPlatformId('nonexistent')
const undefinedResult = getPlatformId(undefined)
expect(typeof validResult).toBe('string')
expect(typeof invalidResult).toBe('string')
expect(typeof undefinedResult).toBe('string')
})
})
describe('isValidBrowserPreset', () => {
it('should return true for valid preset keys', () => {
expect(isValidBrowserPreset('ubuntu')).toBe(true)
expect(isValidBrowserPreset('macOS')).toBe(true)
expect(isValidBrowserPreset('windows')).toBe(true)
expect(isValidBrowserPreset('baileys')).toBe(true)
expect(isValidBrowserPreset('appropriate')).toBe(true)
})
it('should return false for invalid preset keys', () => {
expect(isValidBrowserPreset('invalid')).toBe(false)
expect(isValidBrowserPreset('UBUNTU')).toBe(false)
expect(isValidBrowserPreset('Ubuntu')).toBe(false)
expect(isValidBrowserPreset('MacOS')).toBe(false)
})
it('should return false for non-string values', () => {
expect(isValidBrowserPreset(undefined)).toBe(false)
expect(isValidBrowserPreset(null)).toBe(false)
expect(isValidBrowserPreset(123)).toBe(false)
expect(isValidBrowserPreset({})).toBe(false)
expect(isValidBrowserPreset([])).toBe(false)
expect(isValidBrowserPreset(true)).toBe(false)
})
// Critical security test: inherited properties should not match
it('should return false for inherited Object properties (security)', () => {
expect(isValidBrowserPreset('toString')).toBe(false)
expect(isValidBrowserPreset('constructor')).toBe(false)
expect(isValidBrowserPreset('valueOf')).toBe(false)
expect(isValidBrowserPreset('hasOwnProperty')).toBe(false)
expect(isValidBrowserPreset('__proto__')).toBe(false)
expect(isValidBrowserPreset('prototype')).toBe(false)
})
it('should work as type guard', () => {
const input: unknown = 'ubuntu'
if (isValidBrowserPreset(input)) {
// TypeScript should allow this after the type guard
const config = Browsers[input]('Test')
expect(config).toHaveLength(3)
}
})
})
describe('edge cases and robustness', () => {
it('should not throw on any Browsers method call', () => {
expect(() => Browsers.ubuntu('test')).not.toThrow()
expect(() => Browsers.macOS('test')).not.toThrow()
expect(() => Browsers.windows('test')).not.toThrow()
expect(() => Browsers.baileys('test')).not.toThrow()
expect(() => Browsers.appropriate('test')).not.toThrow()
})
it('should not throw on getPlatformId with any input', () => {
expect(() => getPlatformId('valid')).not.toThrow()
expect(() => getPlatformId('')).not.toThrow()
expect(() => getPlatformId(undefined)).not.toThrow()
expect(() => getPlatformId(null)).not.toThrow()
expect(() => getPlatformId({})).not.toThrow()
expect(() => getPlatformId([])).not.toThrow()
expect(() => getPlatformId(Symbol('test'))).not.toThrow()
})
it('all Browsers methods should return readonly-compatible tuples', () => {
const presets = ['ubuntu', 'macOS', 'windows', 'baileys', 'appropriate'] as const
for (const preset of presets) {
const result = Browsers[preset]('Test')
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(3)
expect(result.every(item => typeof item === 'string')).toBe(true)
}
})
it('version strings should never be empty', () => {
const presets = ['ubuntu', 'macOS', 'windows', 'baileys', 'appropriate'] as const
for (const preset of presets) {
const result = Browsers[preset]('Test')
expect(result[2].length).toBeGreaterThan(0)
}
})
})
describe('version detection', () => {
it('should detect a reasonable Ubuntu version', () => {
const version = detectedOSVersions.ubuntu
// Should be at least 18.04 (oldest supported LTS)
const majorVersion = parseFloat(version)
expect(majorVersion).toBeGreaterThanOrEqual(18)
})
it('should detect a reasonable macOS version', () => {
const version = detectedOSVersions.macOS
// Should be at least macOS 10.x or 11+
const majorVersion = parseFloat(version)
expect(majorVersion).toBeGreaterThanOrEqual(10)
})
it('should detect a reasonable Windows version', () => {
const version = detectedOSVersions.windows
// Should start with 10. (Windows 10/11)
expect(version).toMatch(/^10\./)
})
})
})