From 10aad67861dab4f30d5faf858a4fba6841816a5b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 14:55:42 +0000 Subject: [PATCH 1/2] refactor(browser-utils): improve robustness and type safety - Add pre-computed BROWSER_TO_PLATFORM_ID map for better performance - Implement getPlatformName() helper with try-catch for error handling - Add normalizeBrowserKey() for input validation (handles non-string inputs) - Add safeRelease() wrapper with fallback for OS release detection - Export isValidBrowserPreset() type guard for external validation - Add comprehensive JSDoc documentation with examples - Add 25 unit tests covering all edge cases and robustness scenarios Improvements over original code: - Handles undefined/null/invalid input types gracefully - Returns default values instead of crashing on invalid input - Platform map is alphabetically sorted and deduplicated - Constants use 'as const' for better type inference - Module-level caching avoids repeated proto.DeviceProps access Inspired by PR #2303 from WhiskeySockets/Baileys https://claude.ai/code/session_018XbFWEYwCfeeXioFDLvSXV --- src/Utils/browser-utils.ts | 177 ++++++++++++++++++-- src/__tests__/Utils/browser-utils.test.ts | 186 ++++++++++++++++++++++ 2 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 src/__tests__/Utils/browser-utils.test.ts diff --git a/src/Utils/browser-utils.ts b/src/Utils/browser-utils.ts index ee2e42dc..7659c4c5 100644 --- a/src/Utils/browser-utils.ts +++ b/src/Utils/browser-utils.ts @@ -2,30 +2,175 @@ import { platform, release } from 'os' 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 + +/** + * Maps Node.js platform identifiers to WhatsApp-recognized platform names. + * Values of `undefined` will fall back to DEFAULT_PLATFORM_NAME. + */ +const PLATFORM_MAP: Readonly> = { 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. + */ +const BROWSER_TO_PLATFORM_ID: ReadonlyMap = (() => { + const platformType = proto.DeviceProps?.PlatformType + if (!platformType || typeof platformType !== 'object') { + return new Map() + } + + const entries: Array<[string, string]> = [] + for (const [key, value] of Object.entries(platformType)) { + // PlatformType has both numeric values and reverse mappings + // We only want the string keys with numeric values + if (typeof value === 'number' && typeof key === 'string' && !/^\d+$/.test(key)) { + entries.push([key.toUpperCase(), value.toString()]) + } + } + return new Map(entries) +})() + +// ============================================================ +// 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 + * @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 +} + +/** + * Safely gets the OS release version with fallback + * + * @returns OS release string or empty string on failure + */ +const safeRelease = (): string => { + try { + return release() + } catch { + return '' + } +} + +// ============================================================ +// Exported Constants +// ============================================================ + +/** + * Browser configuration presets for WhatsApp device registration. + * Each factory returns a tuple of [platform, browser, version]. + * + * @example + * // Use Ubuntu preset + * const config = Browsers.ubuntu('Chrome') + * // Returns: ['Ubuntu', 'Chrome', '22.04.4'] + * + * @example + * // Use automatic platform detection + * const config = Browsers.appropriate('MyApp') + * // Returns: ['Mac OS', 'MyApp', '23.1.0'] (on macOS) + */ 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, '22.04.4'], + macOS: (browser: string): [string, string, string] => ['Mac OS', browser, '14.4.1'], + windows: (browser: string): [string, string, string] => ['Windows', browser, '10.0.22631'], + baileys: (browser: string): [string, string, string] => ['Baileys', browser, '6.5.0'], + appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, safeRelease()], +} as const + +// ============================================================ +// Exported Functions +// ============================================================ + +/** + * Resolves the platform type ID for a given browser name. + * Uses the WhatsApp protocol buffer definitions for mapping. + * + * @param browser - Browser identifier (e.g., 'chrome', 'firefox', 'safari') + * @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) + * getPlatformId(undefined) // Returns '1' (default, handles invalid input) + */ +export const getPlatformId = (browser: string): 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 platform string is a valid browser preset key. + * Useful for external validation before using Browsers presets. + * + * @param value - Value to check + * @returns True if value is a valid browser preset key + * + * @example + * if (isValidBrowserPreset(userInput)) { + * const config = Browsers[userInput]('MyApp') + * } + */ +export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => { + return typeof value === 'string' && value in Browsers } diff --git a/src/__tests__/Utils/browser-utils.test.ts b/src/__tests__/Utils/browser-utils.test.ts new file mode 100644 index 00000000..e09a11c6 --- /dev/null +++ b/src/__tests__/Utils/browser-utils.test.ts @@ -0,0 +1,186 @@ +import { Browsers, getPlatformId, isValidBrowserPreset } 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).toEqual(['Ubuntu', 'Chrome', '22.04.4']) + }) + + it('should handle empty browser string', () => { + const result = Browsers.ubuntu('') + expect(result).toEqual(['Ubuntu', '', '22.04.4']) + }) + }) + + describe('macOS', () => { + it('should return correct tuple for macOS', () => { + const result = Browsers.macOS('Safari') + expect(result).toEqual(['Mac OS', 'Safari', '14.4.1']) + }) + }) + + describe('windows', () => { + it('should return correct tuple for Windows', () => { + const result = Browsers.windows('Edge') + expect(result).toEqual(['Windows', 'Edge', '10.0.22631']) + }) + }) + + 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]) + }) + }) + }) + + 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', () => { + // @ts-expect-error - Testing runtime behavior with invalid input + const result = getPlatformId(undefined) + expect(result).toBe('1') + }) + + it('should return default ID for null input', () => { + // @ts-expect-error - Testing runtime behavior with invalid input + const result = getPlatformId(null) + expect(result).toBe('1') + }) + + it('should return default ID for number input', () => { + // @ts-expect-error - Testing runtime behavior with invalid input + const result = getPlatformId(123) + expect(result).toBe('1') + }) + + it('should return default ID for object input', () => { + // @ts-expect-error - Testing runtime behavior with invalid input + const result = getPlatformId({ browser: 'chrome' }) + expect(result).toBe('1') + }) + + it('should always return a string', () => { + const validResult = getPlatformId('chrome') + const invalidResult = getPlatformId('nonexistent') + expect(typeof validResult).toBe('string') + expect(typeof invalidResult).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) + }) + + 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) + }) + + 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() + // @ts-expect-error - Testing runtime behavior + expect(() => getPlatformId(undefined)).not.toThrow() + // @ts-expect-error - Testing runtime behavior + expect(() => getPlatformId(null)).not.toThrow() + // @ts-expect-error - Testing runtime behavior + expect(() => getPlatformId({})).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) + } + }) + }) +}) From d2e599a617122245ff2932ae9c0dbb3617946534 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 15:26:46 +0000 Subject: [PATCH 2/2] feat(browser-utils): add automatic OS version detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements automatic detection of OS versions at runtime instead of hardcoded values. This ensures the library reports accurate platform versions to WhatsApp without manual updates. Version Detection: - Linux: Reads /etc/os-release for distribution version (e.g., '24.04.1') - macOS: Converts Darwin kernel version to macOS version (e.g., Darwin 24.x → macOS 15.x) - Windows: Uses os.release() directly (already returns correct format) Security Fixes (from PR #62 review): - Fix isValidBrowserPreset() to use Object.prototype.hasOwnProperty.call() to prevent matching inherited properties like 'toString' or 'constructor' - Fix getPlatformId() signature to accept 'unknown' type for runtime safety Other Improvements: - Add FALLBACK_VERSIONS constant with updated 2025 versions - Add DARWIN_TO_MACOS mapping for accurate macOS version conversion - Export detectedOSVersions for debugging and logging - Improve JSDoc documentation with accurate examples - Add 36 comprehensive unit tests (up from 25) The version is detected once at module load and cached for consistent behavior throughout the application lifecycle. https://claude.ai/code/session_018XbFWEYwCfeeXioFDLvSXV --- src/Utils/browser-utils.ts | 268 ++++++++++++++++++++-- src/__tests__/Utils/browser-utils.test.ts | 117 +++++++++- 2 files changed, 348 insertions(+), 37 deletions(-) diff --git a/src/Utils/browser-utils.ts b/src/Utils/browser-utils.ts index 7659c4c5..bb8b4a19 100644 --- a/src/Utils/browser-utils.ts +++ b/src/Utils/browser-utils.ts @@ -1,4 +1,5 @@ import { platform, release } from 'os' +import { readFileSync, existsSync } from 'fs' import { proto } from '../../WAProto/index.js' import type { BrowsersMap } from '../Types' @@ -17,6 +18,40 @@ const DEFAULT_PLATFORM_NAME = 'Ubuntu' as const */ 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> = { + 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. @@ -42,6 +77,9 @@ const PLATFORM_MAP: Readonly> = { /** * 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 = (() => { const platformType = proto.DeviceProps?.PlatformType @@ -51,8 +89,6 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap = (() => { const entries: Array<[string, string]> = [] for (const [key, value] of Object.entries(platformType)) { - // PlatformType has both numeric values and reverse mappings - // We only want the string keys with numeric values if (typeof value === 'number' && typeof key === 'string' && !/^\d+$/.test(key)) { entries.push([key.toUpperCase(), value.toString()]) } @@ -60,6 +96,151 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap = (() => { 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 // ============================================================ @@ -82,7 +263,7 @@ const getPlatformName = (): string => { /** * Normalizes a browser identifier for platform type lookup. * - * @param browser - The browser identifier to normalize + * @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 => { @@ -94,15 +275,29 @@ const normalizeBrowserKey = (browser: unknown): string | null => { } /** - * Safely gets the OS release version with fallback + * Gets the appropriate OS version for the current platform. + * Uses automatic detection with fallback to sensible defaults. * - * @returns OS release string or empty string on failure + * @returns OS version string appropriate for the current platform */ -const safeRelease = (): string => { +const getAppropriateVersion = (): string => { try { - return release() + 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 '' + return DEFAULT_OS_VERSION } } @@ -114,24 +309,35 @@ const safeRelease = (): string => { * Browser configuration presets for WhatsApp device registration. * Each factory returns a tuple of [platform, browser, version]. * - * @example - * // Use Ubuntu preset - * const config = Browsers.ubuntu('Chrome') - * // Returns: ['Ubuntu', 'Chrome', '22.04.4'] + * 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 automatic platform detection + * // 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', '23.1.0'] (on macOS) + * // Returns: ['Mac OS', 'MyApp', '15.2'] (on macOS Sequoia) */ export const Browsers: BrowsersMap = { - ubuntu: (browser: string): [string, string, string] => ['Ubuntu', browser, '22.04.4'], - macOS: (browser: string): [string, string, string] => ['Mac OS', browser, '14.4.1'], - windows: (browser: string): [string, string, string] => ['Windows', browser, '10.0.22631'], - baileys: (browser: string): [string, string, string] => ['Baileys', browser, '6.5.0'], - appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, safeRelease()], + 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 // ============================================================ @@ -140,7 +346,11 @@ export const Browsers: BrowsersMap = { * Resolves the platform type ID for a given browser name. * Uses the WhatsApp protocol buffer definitions for mapping. * - * @param browser - Browser identifier (e.g., 'chrome', 'firefox', 'safari') + * 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 @@ -148,9 +358,8 @@ export const Browsers: BrowsersMap = { * getPlatformId('CHROME') // Returns '1' (case-insensitive) * getPlatformId('firefox') // Returns platform-specific ID * getPlatformId('') // Returns '1' (default) - * getPlatformId(undefined) // Returns '1' (default, handles invalid input) */ -export const getPlatformId = (browser: string): string => { +export const getPlatformId = (browser: unknown): string => { const key = normalizeBrowserKey(browser) if (key === null) { return DEFAULT_PLATFORM_ID @@ -160,11 +369,20 @@ export const getPlatformId = (browser: string): string => { } /** - * Type guard to check if a platform string is a valid browser preset key. + * 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 + * @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)) { @@ -172,5 +390,5 @@ export const getPlatformId = (browser: string): string => { * } */ export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => { - return typeof value === 'string' && value in Browsers + return typeof value === 'string' && Object.prototype.hasOwnProperty.call(Browsers, value) } diff --git a/src/__tests__/Utils/browser-utils.test.ts b/src/__tests__/Utils/browser-utils.test.ts index e09a11c6..ad29a427 100644 --- a/src/__tests__/Utils/browser-utils.test.ts +++ b/src/__tests__/Utils/browser-utils.test.ts @@ -1,30 +1,44 @@ -import { Browsers, getPlatformId, isValidBrowserPreset } from '../../Utils/browser-utils' +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).toEqual(['Ubuntu', 'Chrome', '22.04.4']) + 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).toEqual(['Ubuntu', '', '22.04.4']) + 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).toEqual(['Mac OS', 'Safari', '14.4.1']) + 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).toEqual(['Windows', 'Edge', '10.0.22631']) + expect(result[0]).toBe('Windows') + expect(result[1]).toBe('Edge') + expect(result[2]).toMatch(/^\d+\.\d+\.\d+/) // Version format like '10.0.26100' }) }) @@ -51,6 +65,41 @@ describe('browser-utils', () => { // 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) }) }) @@ -89,34 +138,37 @@ describe('browser-utils', () => { }) it('should return default ID for undefined input', () => { - // @ts-expect-error - Testing runtime behavior with invalid input const result = getPlatformId(undefined) expect(result).toBe('1') }) it('should return default ID for null input', () => { - // @ts-expect-error - Testing runtime behavior with invalid input const result = getPlatformId(null) expect(result).toBe('1') }) it('should return default ID for number input', () => { - // @ts-expect-error - Testing runtime behavior with invalid input const result = getPlatformId(123) expect(result).toBe('1') }) it('should return default ID for object input', () => { - // @ts-expect-error - Testing runtime behavior with invalid 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') }) }) @@ -133,6 +185,7 @@ describe('browser-utils', () => { 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', () => { @@ -141,6 +194,17 @@ describe('browser-utils', () => { 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', () => { @@ -165,12 +229,11 @@ describe('browser-utils', () => { it('should not throw on getPlatformId with any input', () => { expect(() => getPlatformId('valid')).not.toThrow() expect(() => getPlatformId('')).not.toThrow() - // @ts-expect-error - Testing runtime behavior expect(() => getPlatformId(undefined)).not.toThrow() - // @ts-expect-error - Testing runtime behavior expect(() => getPlatformId(null)).not.toThrow() - // @ts-expect-error - Testing runtime behavior expect(() => getPlatformId({})).not.toThrow() + expect(() => getPlatformId([])).not.toThrow() + expect(() => getPlatformId(Symbol('test'))).not.toThrow() }) it('all Browsers methods should return readonly-compatible tuples', () => { @@ -182,5 +245,35 @@ describe('browser-utils', () => { 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\./) + }) }) })