api-ape 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -1,286 +1,348 @@
1
1
  /**
2
- * Robust User-Agent Parser
3
- * Zero-dependency replacement for ua-parser-js
4
- * Handles browsers, OS, devices, bots (including AI), and edge cases
2
+ * @fileoverview Robust User-Agent Parser - Zero Dependencies
3
+ *
4
+ * This module provides a comprehensive User-Agent string parser that extracts
5
+ * detailed information about the client's browser, operating system, device,
6
+ * CPU architecture, and bot status. It has zero external dependencies and
7
+ * handles a wide variety of user agents including:
8
+ *
9
+ * - Modern browsers (Chrome, Firefox, Safari, Edge, Opera, Brave, etc.)
10
+ * - Mobile browsers (iOS Safari, Chrome Mobile, Samsung Internet, etc.)
11
+ * - AI bots (ChatGPT, Claude, GPTBot, Perplexity, etc.)
12
+ * - Traditional crawlers (Googlebot, Bingbot, etc.)
13
+ * - Headless browsers (HeadlessChrome, PhantomJS, Puppeteer, etc.)
14
+ * - In-app browsers (Facebook, Instagram, WhatsApp, etc.)
15
+ * - Game consoles (PlayStation, Xbox, Nintendo)
16
+ * - Smart TVs and set-top boxes
17
+ *
18
+ * The parser is designed to be:
19
+ * - **Fast**: Simple regex matching with early termination
20
+ * - **Accurate**: Handles edge cases and common variations
21
+ * - **Safe**: Returns null values instead of throwing errors
22
+ * - **Comprehensive**: Extracts browser, engine, OS, device, and CPU info
23
+ *
24
+ * @module server/utils/parseUserAgent
25
+ * @see {@link module:server/utils/userAgent/patterns} - Pattern definitions
26
+ *
27
+ * @example
28
+ * const parseUserAgent = require('./parseUserAgent')
29
+ *
30
+ * const result = parseUserAgent(
31
+ * 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
32
+ * '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
33
+ * )
34
+ *
35
+ * console.log(result)
36
+ * // {
37
+ * // browser: { name: 'Chrome', version: '120.0.0.0', major: '120' },
38
+ * // engine: { name: 'Blink', version: '120.0.0.0' },
39
+ * // os: { name: 'Windows', version: '10' },
40
+ * // device: { type: null, vendor: null, model: null },
41
+ * // cpu: { architecture: 'amd64' },
42
+ * // isBot: false,
43
+ * // raw: '...'
44
+ * // }
45
+ *
46
+ * @example
47
+ * // Detect AI bots
48
+ * const result = parseUserAgent('ClaudeBot/1.0')
49
+ * console.log(result.browser.name) // 'ClaudeBot'
50
+ * console.log(result.isBot) // true
51
+ *
52
+ * @example
53
+ * // Handle mobile devices
54
+ * const result = parseUserAgent(
55
+ * 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...'
56
+ * )
57
+ * console.log(result.device.type) // 'mobile'
58
+ * console.log(result.device.vendor) // 'Apple'
59
+ * console.log(result.os.name) // 'iOS'
5
60
  */
6
61
 
7
- // Browser detection patterns - ORDER MATTERS (specific before generic)
8
- const BROWSERS = [
9
- // AI Bots first (most specific)
10
- { name: 'ChatGPT-User', pattern: /ChatGPT-User\/?([\d.]*)/i },
11
- { name: 'GPTBot', pattern: /GPTBot\/?([\d.]*)/i },
12
- { name: 'OAI-SearchBot', pattern: /OAI-SearchBot\/?([\d.]*)/i },
13
- { name: 'ClaudeBot', pattern: /ClaudeBot\/?([\d.]*)/i },
14
- { name: 'Claude-User', pattern: /Claude-User\/?([\d.]*)/i },
15
- { name: 'Claude-SearchBot', pattern: /Claude-SearchBot\/?([\d.]*)/i },
16
- { name: 'Claude-Web', pattern: /Claude-Web\/?([\d.]*)/i },
17
- { name: 'PerplexityBot', pattern: /PerplexityBot\/?([\d.]*)/i },
18
- { name: 'Perplexity-User', pattern: /Perplexity-User\/?([\d.]*)/i },
19
- { name: 'Google-Extended', pattern: /Google-Extended/i },
62
+ const {
63
+ BROWSERS,
64
+ OS_PATTERNS,
65
+ ENGINES,
66
+ DEVICE_PATTERNS,
67
+ DEVICE_VENDORS,
68
+ CPU_PATTERNS,
69
+ BOT_PATTERNS,
70
+ MODEL_PATTERNS,
71
+ } = require("./userAgent/patterns");
20
72
 
21
- // Traditional bots
22
- { name: 'Googlebot', pattern: /Googlebot\/?([\d.]*)/i },
23
- { name: 'Bingbot', pattern: /bingbot\/?([\d.]*)/i },
24
- { name: 'YandexBot', pattern: /YandexBot\/?([\d.]*)/i },
25
- { name: 'DuckDuckBot', pattern: /DuckDuckBot\/?([\d.]*)/i },
26
- { name: 'Slurp', pattern: /Slurp/i },
27
- { name: 'Baiduspider', pattern: /Baiduspider\/?([\d.]*)/i },
28
- { name: 'curl', pattern: /curl\/?([\d.]*)/i },
29
- { name: 'wget', pattern: /Wget\/?([\d.]*)/i },
30
- { name: 'HeadlessChrome', pattern: /HeadlessChrome\/?([\d.]*)/i },
31
- { name: 'PhantomJS', pattern: /PhantomJS\/?([\d.]*)/i },
32
- { name: 'Puppeteer', pattern: /Puppeteer/i },
33
- { name: 'Playwright', pattern: /Playwright/i },
34
-
35
- // WebViews / In-app browsers (before generic browsers)
36
- { name: 'Facebook', pattern: /\bFB[\w_]*\/?([\d.]*)/i },
37
- { name: 'Instagram', pattern: /Instagram\s?([\d.]*)/i },
38
- { name: 'Twitter', pattern: /Twitter/i },
39
- { name: 'TikTok', pattern: /TikTok/i },
40
- { name: 'Snapchat', pattern: /Snapchat/i },
41
- { name: 'LinkedIn', pattern: /LinkedInApp/i },
42
- { name: 'Pinterest', pattern: /Pinterest/i },
43
- { name: 'WhatsApp', pattern: /WhatsApp\/?([\d.]*)/i },
44
- { name: 'Telegram', pattern: /TelegramBot/i },
45
-
46
- // Chromium-based (before Chrome)
47
- { name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/?([\d.]*)/i },
48
- { name: 'Opera', pattern: /(?:OPR|Opera)\/?([\d.]*)/i },
49
- { name: 'Brave', pattern: /Brave\/?([\d.]*)/i },
50
- { name: 'Vivaldi', pattern: /Vivaldi\/?([\d.]*)/i },
51
- { name: 'Yandex', pattern: /YaBrowser\/?([\d.]*)/i },
52
- { name: 'Samsung Internet', pattern: /SamsungBrowser\/?([\d.]*)/i },
53
- { name: 'UC Browser', pattern: /UCBrowser\/?([\d.]*)/i },
54
- { name: 'QQ Browser', pattern: /QQBrowser\/?([\d.]*)/i },
55
- { name: 'Whale', pattern: /Whale\/?([\d.]*)/i },
56
-
57
- // Major browsers
58
- { name: 'Chrome', pattern: /(?:Chrome|CriOS)\/?([\d.]*)/i },
59
- { name: 'Firefox', pattern: /(?:Firefox|FxiOS)\/?([\d.]*)/i },
60
- { name: 'Safari', pattern: /Version\/([\d.]*)\s.*Safari/i },
61
-
62
- // Legacy IE
63
- { name: 'IE', pattern: /(?:MSIE\s|Trident.*rv:)([\d.]*)/i },
64
- ];
65
-
66
- // OS detection patterns - ORDER MATTERS (specific before generic)
67
- const OS_PATTERNS = [
68
- { name: 'iOS', pattern: /(?:iPhone|iPad|iPod).*?OS\s([\d_]+)/i, versionSep: '_' },
69
- // Android - exclude "like Android" (e.g., Kindle UA)
70
- { name: 'Android', pattern: /(?<!like\s)Android\s?([\d.]*)/i },
71
- { name: 'macOS', pattern: /Mac OS X\s?([\d_\.]*)/i, versionSep: '_' },
72
- {
73
- name: 'Windows', pattern: /Windows NT\s?([\d.]*)/i, versionMap: {
74
- '10.0': '10', '6.3': '8.1', '6.2': '8', '6.1': '7', '6.0': 'Vista', '5.1': 'XP'
75
- }
76
- },
77
- { name: 'Chrome OS', pattern: /CrOS\s\w+\s([\d.]*)/i },
78
- // Specific distros before generic Linux
79
- { name: 'Ubuntu', pattern: /Ubuntu/i },
80
- { name: 'Fedora', pattern: /Fedora/i },
81
- { name: 'FreeBSD', pattern: /FreeBSD/i },
82
- { name: 'Linux', pattern: /Linux/i },
83
- ];
73
+ /**
74
+ * @typedef {Object} BrowserInfo
75
+ * Information about the detected browser.
76
+ *
77
+ * @property {string|null} name - Browser name (e.g., 'Chrome', 'Firefox', 'Safari')
78
+ * @property {string|null} version - Full version string (e.g., '120.0.0.0')
79
+ * @property {string|null} major - Major version number (e.g., '120')
80
+ */
84
81
 
85
- // Engine detection patterns
86
- const ENGINES = [
87
- { name: 'Blink', pattern: /Chrome\/([\d.]+)/i }, // Modern Chrome, Edge, Opera
88
- { name: 'Gecko', pattern: /Gecko\/([\d.]+)/i },
89
- { name: 'WebKit', pattern: /AppleWebKit\/([\d.]+)/i },
90
- { name: 'Trident', pattern: /Trident\/([\d.]+)/i },
91
- { name: 'EdgeHTML', pattern: /Edge\/([\d.]+)/i },
92
- { name: 'Presto', pattern: /Presto\/([\d.]+)/i },
93
- ];
82
+ /**
83
+ * @typedef {Object} EngineInfo
84
+ * Information about the browser's rendering engine.
85
+ *
86
+ * @property {string|null} name - Engine name (e.g., 'Blink', 'Gecko', 'WebKit')
87
+ * @property {string|null} version - Engine version string
88
+ */
94
89
 
95
- // Device type patterns - ORDER MATTERS (console before tablet/mobile to catch Xbox/PlayStation first)
96
- const DEVICE_PATTERNS = [
97
- { type: 'console', pattern: /PlayStation|Xbox|Nintendo/i },
98
- { type: 'tablet', pattern: /iPad|Android(?!.*Mobile)|Tablet|PlayBook/i },
99
- { type: 'mobile', pattern: /Mobile|Android.*Mobile|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Opera Mobi/i },
100
- { type: 'smarttv', pattern: /SmartTV|Smart-TV|GoogleTV|AppleTV|BRAVIA|WebOS|Tizen|HbbTV|NetCast/i },
101
- { type: 'wearable', pattern: /Watch|Fitbit/i },
102
- { type: 'embedded', pattern: /Embedded/i },
103
- ];
90
+ /**
91
+ * @typedef {Object} OSInfo
92
+ * Information about the operating system.
93
+ *
94
+ * @property {string|null} name - OS name (e.g., 'Windows', 'macOS', 'iOS', 'Android')
95
+ * @property {string|null} version - OS version (e.g., '10', '14.0', '17.0')
96
+ */
104
97
 
105
- // Device vendor/model patterns
106
- const DEVICE_VENDORS = [
107
- { vendor: 'Apple', pattern: /iPhone|iPad|iPod|Macintosh|AppleTV/i },
108
- { vendor: 'Samsung', pattern: /Samsung|SM-|GT-/i },
109
- { vendor: 'Huawei', pattern: /Huawei|HUAWEI/i },
110
- // Xiaomi: brand names + model codes (e.g., 24030PN60G, M2102J20SG)
111
- { vendor: 'Xiaomi', pattern: /Xiaomi|Mi\s|Redmi|\b\d{5}[A-Z]{2}\d{2}[A-Z]\b|\bM\d{4}[A-Z]\d{2}[A-Z]{2}\b/i },
112
- { vendor: 'Google', pattern: /Pixel|Nexus/i },
113
- { vendor: 'OnePlus', pattern: /OnePlus|ONEPLUS/i },
114
- { vendor: 'LG', pattern: /LG[-;\/\s]/i },
115
- // Sony: brand + Xperia + tablet codes (SGP)
116
- { vendor: 'Sony', pattern: /Sony|Xperia|PlayStation|\bSGP\d+\b/i },
117
- { vendor: 'Motorola', pattern: /Motorola|Moto\s|\bmoto\s/i },
118
- { vendor: 'HTC', pattern: /HTC/i },
119
- { vendor: 'Nokia', pattern: /Nokia/i },
120
- { vendor: 'Oppo', pattern: /OPPO/i },
121
- { vendor: 'Vivo', pattern: /vivo/i },
122
- { vendor: 'Realme', pattern: /RMX\d/i },
123
- // Microsoft: Xbox, Surface, and "Microsoft;" in UA
124
- { vendor: 'Microsoft', pattern: /Xbox|Surface|Microsoft;/i },
125
- { vendor: 'Nintendo', pattern: /Nintendo/i },
126
- ];
98
+ /**
99
+ * @typedef {Object} DeviceInfo
100
+ * Information about the device.
101
+ *
102
+ * @property {string|null} type - Device type: 'mobile', 'tablet', 'console', 'smarttv', 'wearable', 'embedded', or null for desktop
103
+ * @property {string|null} vendor - Device manufacturer (e.g., 'Apple', 'Samsung', 'Google')
104
+ * @property {string|null} model - Device model (e.g., 'iPhone', 'Pixel 8 Pro', 'SM-G998B')
105
+ */
127
106
 
128
- // CPU architecture patterns
129
- const CPU_PATTERNS = [
130
- { architecture: 'arm64', pattern: /aarch64|arm64/i },
131
- { architecture: 'arm', pattern: /arm(?!64)/i },
132
- { architecture: 'amd64', pattern: /x64|x86_64|amd64|Win64|WOW64/i },
133
- { architecture: 'ia32', pattern: /x86|i[36]86/i },
134
- ];
107
+ /**
108
+ * @typedef {Object} CPUInfo
109
+ * Information about the CPU architecture.
110
+ *
111
+ * @property {string|null} architecture - CPU architecture: 'arm64', 'arm', 'amd64', 'ia32', or null
112
+ */
135
113
 
136
- // Bot detection - comprehensive list
137
- const BOT_PATTERNS = [
138
- // AI bots
139
- /ChatGPT|GPTBot|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User|Google-Extended/i,
140
- // Traditional search
141
- /bot|crawl|spider|slurp|search|fetch|monitor|check|scan/i,
142
- // Specific bots
143
- /Googlebot|Bingbot|YandexBot|DuckDuckBot|Baiduspider|Sogou|Exabot|facebot|ia_archiver/i,
144
- // Tools
145
- /curl|wget|python|java|perl|ruby|php|http|node|axios|got\//i,
146
- // Headless
147
- /HeadlessChrome|PhantomJS|Puppeteer|Playwright|Selenium|WebDriver/i,
148
- ];
114
+ /**
115
+ * @typedef {Object} ParsedUserAgent
116
+ * Complete result from parsing a User-Agent string.
117
+ *
118
+ * @property {BrowserInfo} browser - Browser information
119
+ * @property {EngineInfo} engine - Rendering engine information
120
+ * @property {OSInfo} os - Operating system information
121
+ * @property {DeviceInfo} device - Device information
122
+ * @property {CPUInfo} cpu - CPU architecture information
123
+ * @property {boolean} isBot - True if the user agent appears to be a bot/crawler
124
+ * @property {string|null} raw - The original User-Agent string
125
+ */
149
126
 
150
- // Model extraction patterns
151
- const MODEL_PATTERNS = [
152
- // Apple devices - including iPad16,3 format
153
- { pattern: /(iPad\d+,\d+|iPhone\d+,\d+|iPod\d+,\d+)/, extract: (m) => m[1].replace(/\d+,\d+/, '') },
154
- { pattern: /(iPhone|iPad|iPod)[\s;]/, extract: (m) => m[1] },
155
- // Samsung
156
- { pattern: /(SM-[A-Z0-9]+|GT-[A-Z0-9]+)/i, extract: (m) => m[1] },
157
- // Google Pixel
158
- { pattern: /(Pixel[\s]?\d*[a-z]?\s?(?:Pro|XL)?)/i, extract: (m) => m[1].trim() },
159
- // Nexus
160
- { pattern: /(Nexus\s?\d+[a-z]?)/i, extract: (m) => m[1] },
161
- // Generic Android - "Build/MODEL" or "; MODEL Build"
162
- { pattern: /;\s*([^;)]+)\s*Build\//i, extract: (m) => m[1].trim() },
163
- ];
127
+ /**
128
+ * Creates an empty result object with all null values.
129
+ *
130
+ * Used as the default return value and as a base for building results.
131
+ *
132
+ * @private
133
+ * @function createEmptyResult
134
+ * @param {string|null} ua - The original User-Agent string to store in `raw`
135
+ * @returns {ParsedUserAgent} Empty result object with all null values
136
+ */
137
+ function createEmptyResult(ua) {
138
+ return {
139
+ browser: { name: null, version: null, major: null },
140
+ engine: { name: null, version: null },
141
+ os: { name: null, version: null },
142
+ device: { type: null, vendor: null, model: null },
143
+ cpu: { architecture: null },
144
+ isBot: false,
145
+ raw: ua || null,
146
+ };
147
+ }
164
148
 
165
149
  /**
166
- * Parse a User-Agent string and extract browser, OS, device, and bot info
167
- * @param {string|null|undefined} ua - The User-Agent string
168
- * @returns {Object} Parsed results matching ua-parser-js structure
150
+ * Parses a User-Agent string and extracts detailed client information.
151
+ *
152
+ * This function analyzes the User-Agent string to determine:
153
+ * - **Browser**: Name, version, and major version number
154
+ * - **Engine**: Rendering engine (Blink, Gecko, WebKit, etc.)
155
+ * - **OS**: Operating system name and version
156
+ * - **Device**: Type (mobile/tablet/etc.), vendor, and model
157
+ * - **CPU**: Architecture (arm64, amd64, etc.)
158
+ * - **Bot status**: Whether this appears to be an automated client
159
+ *
160
+ * The parser handles many edge cases:
161
+ * - Safari detection when Chrome isn't present
162
+ * - Version number normalization (e.g., Windows NT versions)
163
+ * - Multiple bot detection patterns (AI bots, crawlers, headless browsers)
164
+ *
165
+ * @function parseUserAgent
166
+ * @param {string|null|undefined} ua - The User-Agent string to parse
167
+ * @returns {ParsedUserAgent} Parsed information about the client
168
+ *
169
+ * @example
170
+ * // Parse a Chrome user agent
171
+ * const result = parseUserAgent(
172
+ * 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
173
+ * 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
174
+ * )
175
+ *
176
+ * console.log(result.browser.name) // 'Chrome'
177
+ * console.log(result.browser.major) // '120'
178
+ * console.log(result.os.name) // 'macOS'
179
+ * console.log(result.os.version) // '10.15.7'
180
+ * console.log(result.device.type) // null (desktop)
181
+ * console.log(result.isBot) // false
182
+ *
183
+ * @example
184
+ * // Parse a mobile Safari user agent
185
+ * const result = parseUserAgent(
186
+ * 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' +
187
+ * 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
188
+ * )
189
+ *
190
+ * console.log(result.browser.name) // 'Safari'
191
+ * console.log(result.os.name) // 'iOS'
192
+ * console.log(result.device.type) // 'mobile'
193
+ * console.log(result.device.vendor) // 'Apple'
194
+ * console.log(result.device.model) // 'iPhone'
195
+ *
196
+ * @example
197
+ * // Detect an AI bot
198
+ * const result = parseUserAgent('ChatGPT-User')
199
+ *
200
+ * console.log(result.browser.name) // 'ChatGPT-User'
201
+ * console.log(result.isBot) // true
202
+ *
203
+ * @example
204
+ * // Handle null/undefined input
205
+ * const result = parseUserAgent(null)
206
+ *
207
+ * console.log(result.browser.name) // null
208
+ * console.log(result.raw) // null
209
+ *
210
+ * @example
211
+ * // Use in request handler
212
+ * app.use((req, res, next) => {
213
+ * req.userAgent = parseUserAgent(req.headers['user-agent'])
214
+ *
215
+ * // Block bots from certain endpoints
216
+ * if (req.userAgent.isBot && req.path.startsWith('/api/')) {
217
+ * return res.status(403).json({ error: 'Bots not allowed' })
218
+ * }
219
+ *
220
+ * // Serve mobile-optimized content
221
+ * if (req.userAgent.device.type === 'mobile') {
222
+ * req.isMobile = true
223
+ * }
224
+ *
225
+ * next()
226
+ * })
169
227
  */
170
228
  function parseUserAgent(ua) {
171
- // Handle null/undefined
172
- if (ua == null || typeof ua !== 'string') {
173
- return createEmptyResult(ua);
174
- }
229
+ // Handle null, undefined, or non-string input
230
+ if (ua == null || typeof ua !== "string") {
231
+ return createEmptyResult(ua);
232
+ }
175
233
 
176
- const result = {
177
- browser: { name: null, version: null, major: null },
178
- engine: { name: null, version: null },
179
- os: { name: null, version: null },
180
- device: { type: null, vendor: null, model: null },
181
- cpu: { architecture: null },
182
- isBot: false,
183
- raw: ua,
184
- };
234
+ const result = createEmptyResult(ua);
235
+ result.raw = ua;
185
236
 
186
- // Detect browser
187
- for (const { name, pattern } of BROWSERS) {
188
- const match = ua.match(pattern);
189
- if (match) {
190
- result.browser.name = name;
191
- result.browser.version = match[1] || null;
192
- result.browser.major = match[1] ? match[1].split('.')[0] : null;
193
- break;
194
- }
237
+ // =========================================================================
238
+ // BROWSER DETECTION
239
+ // =========================================================================
240
+ // Try each browser pattern in priority order (most specific first)
241
+ for (const { name, pattern } of BROWSERS) {
242
+ const match = ua.match(pattern);
243
+ if (match) {
244
+ result.browser.name = name;
245
+ result.browser.version = match[1] || null;
246
+ result.browser.major = match[1] ? match[1].split(".")[0] : null;
247
+ break;
195
248
  }
249
+ }
196
250
 
197
- // Fallback: Safari without version
198
- if (!result.browser.name && /Safari/i.test(ua) && !/Chrome/i.test(ua)) {
199
- result.browser.name = 'Safari';
200
- const safariMatch = ua.match(/Safari\/([\d.]+)/i);
201
- result.browser.version = safariMatch ? safariMatch[1] : null;
202
- result.browser.major = result.browser.version ? result.browser.version.split('.')[0] : null;
203
- }
251
+ // Special case: Safari detection when Chrome isn't present
252
+ // Many browsers include "Safari" in their UA, but real Safari doesn't have "Chrome"
253
+ if (!result.browser.name && /Safari/i.test(ua) && !/Chrome/i.test(ua)) {
254
+ result.browser.name = "Safari";
255
+ const m = ua.match(/Safari\/([\d.]+)/i);
256
+ result.browser.version = m ? m[1] : null;
257
+ result.browser.major = result.browser.version
258
+ ? result.browser.version.split(".")[0]
259
+ : null;
260
+ }
204
261
 
205
- // Detect engine
206
- for (const { name, pattern } of ENGINES) {
207
- const match = ua.match(pattern);
208
- if (match) {
209
- result.engine.name = name;
210
- result.engine.version = match[1] || null;
211
- break;
212
- }
262
+ // =========================================================================
263
+ // ENGINE DETECTION
264
+ // =========================================================================
265
+ for (const { name, pattern } of ENGINES) {
266
+ const match = ua.match(pattern);
267
+ if (match) {
268
+ result.engine.name = name;
269
+ result.engine.version = match[1] || null;
270
+ break;
213
271
  }
272
+ }
214
273
 
215
- // Detect OS
216
- for (const { name, pattern, versionSep, versionMap } of OS_PATTERNS) {
217
- const match = ua.match(pattern);
218
- if (match) {
219
- result.os.name = name;
220
- let version = match[1] || null;
221
- if (version && versionSep) {
222
- version = version.replace(new RegExp(versionSep, 'g'), '.');
223
- }
224
- if (version && versionMap && versionMap[version]) {
225
- version = versionMap[version];
226
- }
227
- result.os.version = version;
228
- break;
229
- }
230
- }
274
+ // =========================================================================
275
+ // OS DETECTION
276
+ // =========================================================================
277
+ for (const { name, pattern, versionSep, versionMap } of OS_PATTERNS) {
278
+ const match = ua.match(pattern);
279
+ if (match) {
280
+ result.os.name = name;
281
+ let version = match[1] || null;
231
282
 
232
- // Detect device type (check tablet before mobile due to iPad containing 'Mobile')
233
- for (const { type, pattern } of DEVICE_PATTERNS) {
234
- if (pattern.test(ua)) {
235
- result.device.type = type;
236
- break;
237
- }
283
+ // Replace version separator (e.g., "_" with "." for iOS/macOS)
284
+ if (version && versionSep) {
285
+ version = version.replace(new RegExp(versionSep, "g"), ".");
286
+ }
287
+
288
+ // Map version numbers (e.g., "6.1" to "7" for Windows)
289
+ if (version && versionMap && versionMap[version]) {
290
+ version = versionMap[version];
291
+ }
292
+
293
+ result.os.version = version;
294
+ break;
238
295
  }
296
+ }
239
297
 
240
- // Detect device vendor
241
- for (const { vendor, pattern } of DEVICE_VENDORS) {
242
- if (pattern.test(ua)) {
243
- result.device.vendor = vendor;
244
- break;
245
- }
298
+ // =========================================================================
299
+ // DEVICE TYPE DETECTION
300
+ // =========================================================================
301
+ for (const { type, pattern } of DEVICE_PATTERNS) {
302
+ if (pattern.test(ua)) {
303
+ result.device.type = type;
304
+ break;
246
305
  }
306
+ }
247
307
 
248
- // Detect device model
249
- for (const { pattern, extract } of MODEL_PATTERNS) {
250
- const match = ua.match(pattern);
251
- if (match) {
252
- result.device.model = extract(match);
253
- break;
254
- }
308
+ // =========================================================================
309
+ // DEVICE VENDOR DETECTION
310
+ // =========================================================================
311
+ for (const { vendor, pattern } of DEVICE_VENDORS) {
312
+ if (pattern.test(ua)) {
313
+ result.device.vendor = vendor;
314
+ break;
255
315
  }
316
+ }
256
317
 
257
- // Detect CPU architecture
258
- for (const { architecture, pattern } of CPU_PATTERNS) {
259
- if (pattern.test(ua)) {
260
- result.cpu.architecture = architecture;
261
- break;
262
- }
318
+ // =========================================================================
319
+ // DEVICE MODEL DETECTION
320
+ // =========================================================================
321
+ for (const { pattern, extract } of MODEL_PATTERNS) {
322
+ const match = ua.match(pattern);
323
+ if (match) {
324
+ result.device.model = extract(match);
325
+ break;
263
326
  }
327
+ }
264
328
 
265
- // Detect bot
266
- result.isBot = BOT_PATTERNS.some(pattern => pattern.test(ua));
329
+ // =========================================================================
330
+ // CPU ARCHITECTURE DETECTION
331
+ // =========================================================================
332
+ for (const { architecture, pattern } of CPU_PATTERNS) {
333
+ if (pattern.test(ua)) {
334
+ result.cpu.architecture = architecture;
335
+ break;
336
+ }
337
+ }
267
338
 
268
- return result;
269
- }
339
+ // =========================================================================
340
+ // BOT DETECTION
341
+ // =========================================================================
342
+ // Check all bot patterns - any match indicates a bot
343
+ result.isBot = BOT_PATTERNS.some((pattern) => pattern.test(ua));
270
344
 
271
- /**
272
- * Create an empty result for null/undefined/empty UA
273
- */
274
- function createEmptyResult(ua) {
275
- return {
276
- browser: { name: null, version: null, major: null },
277
- engine: { name: null, version: null },
278
- os: { name: null, version: null },
279
- device: { type: null, vendor: null, model: null },
280
- cpu: { architecture: null },
281
- isBot: false,
282
- raw: ua || null,
283
- };
345
+ return result;
284
346
  }
285
347
 
286
348
  module.exports = parseUserAgent;