api-ape 3.0.2 → 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 +59 -572
  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 -203
  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 +31 -6
  44. package/server/README.md +272 -67
  45. package/server/adapters/README.md +23 -14
  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 +322 -71
  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 -219
  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 -224
  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 -311
  186. package/server/lib/broadcast.js +0 -146
@@ -1,21 +1,149 @@
1
1
  /**
2
- * Extract root domain from a URL
3
- * e.g., "https://sub.example.com:3000/path" -> "example.com"
2
+ * @fileoverview Root Domain Extraction Utility
3
+ *
4
+ * This module provides a utility function to extract the root domain from a URL
5
+ * or hostname. This is useful for security checks like CORS origin validation,
6
+ * where you need to compare domains regardless of subdomains or ports.
7
+ *
8
+ * The function handles various input formats:
9
+ * - Full URLs: `https://sub.example.com:3000/path` → `example.com`
10
+ * - Hostnames with port: `api.example.com:8080` → `example.com`
11
+ * - Simple hostnames: `www.example.com` → `example.com`
12
+ * - Already root domains: `example.com` → `example.com`
13
+ *
14
+ * @module server/security/extractRootDomain
15
+ * @see {@link module:server/security/origin} - Origin security validation
16
+ *
17
+ * @example
18
+ * const extractRootDomain = require('./extractRootDomain')
19
+ *
20
+ * // Full URL with subdomain and port
21
+ * extractRootDomain('https://api.example.com:3000/v1/users')
22
+ * // Returns: 'example.com'
23
+ *
24
+ * // Hostname with subdomain
25
+ * extractRootDomain('www.mysite.org')
26
+ * // Returns: 'mysite.org'
27
+ *
28
+ * // Already a root domain
29
+ * extractRootDomain('example.com')
30
+ * // Returns: 'example.com'
4
31
  */
32
+
33
+ /**
34
+ * Extracts the root domain from a URL or hostname.
35
+ *
36
+ * The root domain is the registrable domain (e.g., `example.com`) without
37
+ * any subdomains (e.g., `www`, `api`, `staging`).
38
+ *
39
+ * Algorithm:
40
+ * 1. If input contains `://`, parse as full URL and extract hostname
41
+ * 2. Otherwise, treat as hostname and remove port if present
42
+ * 3. Split hostname by `.` and take the last two segments
43
+ *
44
+ * **Country Code TLD Handling**:
45
+ * - Handles common ccTLDs like `.co.uk`, `.com.au`, `.me.uk`
46
+ * - Detection: If TLD is 2 chars AND SLD is a known pattern (co, com, net, org, me, ac, gov)
47
+ * - Example: `api.example.co.uk` → `example.co.uk` (not `co.uk`)
48
+ *
49
+ * **Limitations**:
50
+ * - For complex cases (e.g., `.pvt.k12.ma.us`), consider using
51
+ * a proper public suffix list library
52
+ *
53
+ * @function extractRootDomain
54
+ * @param {string} url - Full URL or hostname to extract root domain from
55
+ * @returns {string} The root domain, or empty string if input is falsy
56
+ *
57
+ * @example
58
+ * // Full URL with subdomain
59
+ * extractRootDomain('https://sub.example.com:3000/path')
60
+ * // Returns: 'example.com'
61
+ *
62
+ * @example
63
+ * // Hostname with port
64
+ * extractRootDomain('api.example.com:8080')
65
+ * // Returns: 'example.com'
66
+ *
67
+ * @example
68
+ * // Simple subdomain
69
+ * extractRootDomain('www.example.com')
70
+ * // Returns: 'example.com'
71
+ *
72
+ * @example
73
+ * // Already root domain
74
+ * extractRootDomain('example.com')
75
+ * // Returns: 'example.com'
76
+ *
77
+ * @example
78
+ * // Two-part domain
79
+ * extractRootDomain('localhost')
80
+ * // Returns: 'localhost'
81
+ *
82
+ * @example
83
+ * // Null/undefined input
84
+ * extractRootDomain(null)
85
+ * // Returns: ''
86
+ *
87
+ * @example
88
+ * // Invalid URL falls back gracefully
89
+ * extractRootDomain('not-a-valid-url')
90
+ * // Returns: 'not-a-valid-url'
91
+ */
92
+ // Common second-level domains used with country code TLDs
93
+ const CCTLD_SLDS = new Set(["co", "com", "net", "org", "me", "ac", "gov", "edu"]);
94
+
95
+ /**
96
+ * Check if a domain uses a country code TLD pattern
97
+ * @param {string[]} parts - Domain parts split by '.'
98
+ * @returns {boolean} True if this looks like a ccTLD domain
99
+ * @private
100
+ */
101
+ function isCcTLD(parts) {
102
+ if (parts.length < 3) return false;
103
+ const tld = parts[parts.length - 1];
104
+ const sld = parts[parts.length - 2];
105
+ // TLD must be 2 chars (country code) AND SLD is a known pattern
106
+ return tld.length === 2 && CCTLD_SLDS.has(sld.toLowerCase());
107
+ }
108
+
5
109
  module.exports = function extractRootDomain(url) {
6
- if (!url) return ''
7
- try {
8
- // Handle full URLs
9
- if (url.includes('://')) {
10
- const hostname = new URL(url).hostname
11
- const parts = hostname.split('.')
12
- return parts.length > 2 ? parts.slice(-2).join('.') : hostname
110
+ // Handle null/undefined/empty input
111
+ if (!url) return "";
112
+
113
+ try {
114
+ // Check if this is a full URL (has protocol)
115
+ if (url.includes("://")) {
116
+ // Parse as URL to extract hostname
117
+ const hostname = new URL(url).hostname;
118
+ const parts = hostname.split(".");
119
+
120
+ if (parts.length > 2) {
121
+ // Check for country code TLD (e.g., .co.uk, .com.au, .me.uk)
122
+ if (isCcTLD(parts)) {
123
+ // Include the third-level domain for ccTLDs
124
+ return parts.slice(-3).join(".");
13
125
  }
14
- // Handle hostname:port format
15
- const hostname = url.split(':')[0]
16
- const parts = hostname.split('.')
17
- return parts.length > 2 ? parts.slice(-2).join('.') : hostname
18
- } catch {
19
- return url.split(':')[0]
126
+ return parts.slice(-2).join(".");
127
+ }
128
+ return hostname;
20
129
  }
21
- }
130
+
131
+ // Handle hostname:port format (no protocol)
132
+ // Remove port by splitting on ':' and taking first part
133
+ const hostname = url.split(":")[0];
134
+ const parts = hostname.split(".");
135
+
136
+ if (parts.length > 2) {
137
+ // Check for country code TLD (e.g., .co.uk, .com.au, .me.uk)
138
+ if (isCcTLD(parts)) {
139
+ return parts.slice(-3).join(".");
140
+ }
141
+ return parts.slice(-2).join(".");
142
+ }
143
+ return hostname;
144
+ } catch {
145
+ // If URL parsing fails, try to extract hostname from raw string
146
+ // This handles malformed URLs gracefully
147
+ return url.split(":")[0];
148
+ }
149
+ };
@@ -0,0 +1,51 @@
1
+ # Security Module Files
2
+
3
+ This module provides security features to protect api-ape connections from common web vulnerabilities, including CSRF protection and duplicate request detection. Origin validation is enabled by default.
4
+
5
+ ## Guidelines
6
+
7
+ - **Origin validation is critical** — Changes to `origin.js` affect all connection security; test thoroughly
8
+ - **Domain matching** — Use `extractRootDomain.js` for domain comparisons; don't implement custom logic
9
+ - **Subdomain support** — Origin checks must work with subdomains (e.g., `app.example.com` vs `example.com`)
10
+ - **No configuration required** — Security features should work out-of-the-box with sensible defaults
11
+ - **Fail secure** — When in doubt, reject the connection; false positives are better than security holes
12
+ - **Localhost exceptions** — Development on localhost should work without origin issues
13
+
14
+ ## Directory Structure
15
+
16
+ ```
17
+ security/
18
+ ├── extractRootDomain.js # Domain extraction for origin validation
19
+ ├── origin.js # Origin verification (CSRF protection)
20
+ └── reply.js # Duplicate request protection
21
+ ```
22
+
23
+ ## Files
24
+
25
+ ### `origin.js`
26
+
27
+ Origin validation to prevent Cross-Site Request Forgery (CSRF) attacks:
28
+
29
+ - Validates the `Origin` header against the `Host` header
30
+ - Ensures WebSocket connections only come from the same origin
31
+ - Automatically rejects cross-origin requests
32
+ - Works with both Express.js and raw Node.js servers
33
+ - Returns `true` if valid, `false` to reject connection
34
+
35
+ ### `extractRootDomain.js`
36
+
37
+ Extracts the root domain from a hostname for flexible origin matching:
38
+
39
+ - Handles subdomains (e.g., `app.example.com` → `example.com`)
40
+ - Handles IP addresses and localhost
41
+ - Handles ports in host strings (strips them for comparison)
42
+ - Used by `origin.js` for domain comparison
43
+
44
+ ### `reply.js`
45
+
46
+ Duplicate request protection to prevent replay attacks:
47
+
48
+ - Tracks recently processed `queryId` values
49
+ - Rejects duplicate requests within a configurable time window
50
+ - Prevents attackers from replaying captured WebSocket messages
51
+ - Automatically cleans up expired entries
@@ -1,25 +1,207 @@
1
- const extractRootDomain = require('./extractRootDomain')
1
+ /**
2
+ * @fileoverview Origin Security Check for api-ape WebSocket Connections
3
+ *
4
+ * This module provides Cross-Site Request Forgery (CSRF) protection for
5
+ * WebSocket connections by validating that the request's Origin header
6
+ * matches the server's Host header.
7
+ *
8
+ * ## Why Origin Validation Matters
9
+ *
10
+ * WebSocket connections bypass the Same-Origin Policy that protects regular
11
+ * HTTP requests. Without origin validation, a malicious website could:
12
+ *
13
+ * 1. Connect to your WebSocket server from any domain
14
+ * 2. Send requests using the victim's cookies/credentials
15
+ * 3. Access data or perform actions as the authenticated user
16
+ *
17
+ * ## How It Works
18
+ *
19
+ * ```
20
+ * Browser Request:
21
+ * Origin: https://evil.com
22
+ * Host: api.example.com
23
+ *
24
+ * Validation:
25
+ * extractRootDomain('https://evil.com') → 'evil.com'
26
+ * extractRootDomain('api.example.com') → 'example.com'
27
+ * 'evil.com' !== 'example.com' → REJECT
28
+ * ```
29
+ *
30
+ * ## Allowed Scenarios
31
+ *
32
+ * | Origin | Host | Result | Reason |
33
+ * |----------------------|-----------------------|---------|---------------------------|
34
+ * | https://example.com | api.example.com | ✓ Allow | Same root domain |
35
+ * | https://app.example.com | example.com | ✓ Allow | Same root domain |
36
+ * | (none) | example.com | ✓ Allow | No origin = same-origin |
37
+ * | https://evil.com | example.com | ✗ Reject| Different domains |
38
+ * | https://example.com.evil.com | example.com | ✗ Reject| Fake subdomain attack |
39
+ *
40
+ * ## Subdomains
41
+ *
42
+ * The validation uses root domain comparison (via `extractRootDomain`),
43
+ * which means subdomains of the same root are allowed:
44
+ * - `app.example.com` and `api.example.com` share root `example.com`
45
+ * - Both are allowed to connect to each other
46
+ *
47
+ * @module server/security/origin
48
+ * @see {@link module:server/security/extractRootDomain} for domain extraction
49
+ * @see {@link module:server/socket/open} for usage in connection handling
50
+ *
51
+ * @example <caption>Basic Usage in Connection Handler</caption>
52
+ * const originSecurity = require('./security/origin')
53
+ *
54
+ * function handleConnection(socket, req) {
55
+ * const isSecure = originSecurity(socket, req, (err) => {
56
+ * console.error('Security error:', err)
57
+ * })
58
+ *
59
+ * if (!isSecure) {
60
+ * return // Connection was rejected
61
+ * }
62
+ *
63
+ * // Continue with connection setup...
64
+ * }
65
+ *
66
+ * @example <caption>Integration with api-ape</caption>
67
+ * // This is called internally by socket/open.js
68
+ * const isOk = originSecurity(socket, req, onError)
69
+ *
70
+ * if (!isOk) {
71
+ * // Socket was destroyed, client removed
72
+ * return
73
+ * }
74
+ *
75
+ * // Proceed with message handling
76
+ */
2
77
 
3
- // Helper to get header that works with both Express and raw Node http
78
+ const extractRootDomain = require("./extractRootDomain");
79
+
80
+ /**
81
+ * Get a header value from an HTTP request
82
+ *
83
+ * Handles both Express-style requests (with `.header()` method) and
84
+ * raw Node.js http.IncomingMessage requests (with `.headers` object).
85
+ *
86
+ * @param {http.IncomingMessage|express.Request} req - HTTP request object
87
+ * @param {string} name - Header name (case-insensitive for raw requests)
88
+ * @returns {string|undefined} Header value, or undefined if not present
89
+ * @private
90
+ *
91
+ * @example
92
+ * // Express request
93
+ * getHeader(expressReq, 'Origin') // Uses req.header('Origin')
94
+ *
95
+ * // Raw Node.js request
96
+ * getHeader(httpReq, 'Origin') // Uses req.headers['origin']
97
+ */
4
98
  function getHeader(req, name) {
5
- // Express-style
6
- if (typeof req.header === 'function') {
7
- return req.header(name)
99
+ // Express-style request with .header() method
100
+ if (typeof req.header === "function") {
101
+ return req.header(name);
8
102
  }
9
- // Raw Node.js http request
10
- return req.headers[name.toLowerCase()]
103
+ // Raw Node.js http request - headers are lowercase
104
+ return req.headers[name.toLowerCase()];
11
105
  }
12
106
 
107
+ /**
108
+ * Verify that request origin matches host to prevent CSRF attacks
109
+ *
110
+ * Compares the `Origin` header (where the request came from) with the
111
+ * `Host` header (the server being accessed). If they don't match,
112
+ * the connection is rejected and the socket is destroyed.
113
+ *
114
+ * ## Security Notes
115
+ *
116
+ * - **Missing Origin**: Allowed, as same-origin requests may omit Origin
117
+ * - **Matching Root Domain**: Both headers are reduced to root domains,
118
+ * so `app.example.com` and `api.example.com` both match `example.com`
119
+ * - **Socket Destruction**: Rejected connections have their socket destroyed
120
+ * to immediately terminate the connection
121
+ *
122
+ * ## When Connections Are Rejected
123
+ *
124
+ * - Origin domain doesn't match Host domain
125
+ * - Potential subdomain spoofing (e.g., `example.com.evil.com`)
126
+ *
127
+ * ## When Connections Are Allowed
128
+ *
129
+ * - No Origin header (browser same-origin requests)
130
+ * - Origin root domain matches Host root domain
131
+ *
132
+ * @param {WebSocket} socket - WebSocket instance (must have `.destroy()` method)
133
+ * @param {http.IncomingMessage} req - HTTP upgrade request
134
+ * @param {Function} [onError=console.error] - Error callback for logging rejections
135
+ * @returns {boolean} True if origin is valid and connection should proceed,
136
+ * false if connection was rejected and destroyed
137
+ *
138
+ * @example <caption>Basic Validation</caption>
139
+ * const isValid = originSecurity(socket, req)
140
+ *
141
+ * if (!isValid) {
142
+ * // Socket was destroyed, connection rejected
143
+ * return
144
+ * }
145
+ *
146
+ * // Origin is valid, proceed
147
+ * setupMessageHandlers(socket)
148
+ *
149
+ * @example <caption>With Custom Error Handler</caption>
150
+ * const isValid = originSecurity(socket, req, (errorMessage) => {
151
+ * logger.security.warn(errorMessage, {
152
+ * ip: req.socket.remoteAddress,
153
+ * origin: req.headers.origin,
154
+ * host: req.headers.host
155
+ * })
156
+ * })
157
+ *
158
+ * @example <caption>Scenarios</caption>
159
+ * // Scenario 1: Same domain - ALLOWED
160
+ * // Origin: https://example.com, Host: example.com
161
+ * // extractRootDomain → both 'example.com' → Match ✓
162
+ *
163
+ * // Scenario 2: Subdomain - ALLOWED
164
+ * // Origin: https://app.example.com, Host: api.example.com
165
+ * // extractRootDomain → both 'example.com' → Match ✓
166
+ *
167
+ * // Scenario 3: Different domain - REJECTED
168
+ * // Origin: https://evil.com, Host: example.com
169
+ * // extractRootDomain → 'evil.com' vs 'example.com' → No match ✗
170
+ *
171
+ * // Scenario 4: No origin - ALLOWED
172
+ * // Origin: (not present), Host: example.com
173
+ * // Empty origin doesn't trigger check ✓
174
+ *
175
+ * // Scenario 5: Spoofed subdomain - REJECTED
176
+ * // Origin: https://example.com.evil.com, Host: example.com
177
+ * // extractRootDomain → 'evil.com' vs 'example.com' → No match ✗
178
+ */
13
179
  module.exports = function (socket, req, onError) {
14
- onError = onError || console.error
15
- const origin = extractRootDomain(getHeader(req, 'Origin') || "")
16
- const host = extractRootDomain(getHeader(req, 'Host'))
180
+ // Default error handler to console.error if not provided
181
+ onError = onError || console.error;
182
+
183
+ // Extract root domains for comparison
184
+ const origin = extractRootDomain(getHeader(req, "Origin") || "");
185
+ const host = extractRootDomain(getHeader(req, "Host"));
186
+
187
+ // Check for origin mismatch (only if Origin header is present)
17
188
  if (origin && origin !== host) {
18
- onError("REJECTING socket from " + getHeader(req, 'Origin') + " miss-match with " + getHeader(req, 'Host'))
189
+ // Log the rejection
190
+ onError(
191
+ "REJECTING socket from " +
192
+ getHeader(req, "Origin") +
193
+ " mismatch with " +
194
+ getHeader(req, "Host"),
195
+ );
196
+
197
+ // Destroy the socket to terminate the connection
19
198
  if (socket && socket.destroy) {
20
- socket.destroy()
199
+ socket.destroy();
21
200
  }
22
- return false
201
+
202
+ return false;
23
203
  }
24
- return true
25
- }
204
+
205
+ // Origin is valid (or not present)
206
+ return true;
207
+ };