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,107 +1,548 @@
1
- const replySecurity = require('../security/reply')
2
- const socketOpen = require('../socket/open')
3
- const socketReceive = require('../socket/receive')
4
- const socketSend = require('../socket/send')
5
- const makeid = require('../utils/genId')
6
- const parseUserAgent = require('../utils/parseUserAgent');
7
- const { addClient, removeClient, updateClientEmbed, updateClientSend } = require('./broadcast')
8
-
9
- // connect, beforeSend, beforeReceive, error, afterSend, afterReceive, disconnect
1
+ /**
2
+ * @fileoverview WebSocket Connection Wiring for api-ape Server
3
+ *
4
+ * This module handles the setup and lifecycle of WebSocket connections.
5
+ * It orchestrates the connection between incoming WebSocket clients and
6
+ * the api-ape message handling system.
7
+ *
8
+ * ## Connection Lifecycle
9
+ *
10
+ * ```
11
+ * WebSocket Connection Established
12
+ * │
13
+ * ▼
14
+ * ┌───────────────────────────────────────────────────────────────┐
15
+ * │ wiring() returns webSocketHandler │
16
+ * └───────────────────────────────────────────────────────────────┘
17
+ * │
18
+ * ▼
19
+ * ┌───────────────────────────────────────────────────────────────┐
20
+ * │ webSocketHandler(socket, req) │
21
+ * │ ├── Generate unique clientId │
22
+ * │ ├── Parse user-agent │
23
+ * │ ├── Extract sessionId from cookies │
24
+ * │ ├── Add client to broadcast.clients │
25
+ * │ └── Call onConnect callback (async) │
26
+ * └───────────────────────────────────────────────────────────────┘
27
+ * │
28
+ * ▼
29
+ * ┌───────────────────────────────────────────────────────────────┐
30
+ * │ onConnect resolves with event handlers │
31
+ * │ ├── embed: Custom values for this client │
32
+ * │ ├── onReceive: Called when message received │
33
+ * │ ├── onSend: Called when message sent │
34
+ * │ ├── onError: Called on errors │
35
+ * │ └── onDisconnect: Called when client disconnects │
36
+ * └───────────────────────────────────────────────────────────────┘
37
+ * │
38
+ * ▼
39
+ * ┌───────────────────────────────────────────────────────────────┐
40
+ * │ Connection Active │
41
+ * │ ├── Messages handled by socketReceive │
42
+ * │ ├── Responses sent by socketSend │
43
+ * │ └── Binary data via fileTransfer │
44
+ * └───────────────────────────────────────────────────────────────┘
45
+ * │
46
+ * ▼
47
+ * ┌───────────────────────────────────────────────────────────────┐
48
+ * │ Socket Close Event │
49
+ * │ ├── Remove client from broadcast.clients │
50
+ * │ └── Call onDisconnect handler │
51
+ * └───────────────────────────────────────────────────────────────┘
52
+ * ```
53
+ *
54
+ * ## Event Handler Interface
55
+ *
56
+ * The `onConnect` callback should return an object with optional event handlers:
57
+ *
58
+ * | Handler | Signature | Description |
59
+ * |---------------|-------------------------------------|--------------------------------|
60
+ * | `embed` | `Object` | Values available in controllers|
61
+ * | `onReceive` | `(queryId, data, type) => any` | Called when message received |
62
+ * | `onSend` | `(data, type) => any` | Called when message sent |
63
+ * | `onError` | `(errorString) => void` | Called on errors |
64
+ * | `onDisconnect`| `() => void` | Called when client disconnects |
65
+ *
66
+ * ## Security
67
+ *
68
+ * - Origin validation via `security/origin.js` prevents CSRF attacks
69
+ * - Session ID extracted from cookies for authentication
70
+ * - Client tracking for audit and rate limiting
71
+ *
72
+ * @module server/lib/wiring
73
+ * @see {@link module:server/socket/open} for connection validation
74
+ * @see {@link module:server/socket/receive} for message handling
75
+ * @see {@link module:server/socket/send} for response sending
76
+ * @see {@link module:server/lib/broadcast} for client tracking
77
+ *
78
+ * @example <caption>Basic Usage with onConnect</caption>
79
+ * const wiring = require('./wiring')
80
+ * const controllers = { ping: () => 'pong' }
81
+ *
82
+ * const handler = wiring(controllers, (socket, req, send) => {
83
+ * console.log('Client connected')
84
+ * return {
85
+ * embed: { userId: 'anonymous' },
86
+ * onDisconnect: () => console.log('Client disconnected')
87
+ * }
88
+ * })
89
+ *
90
+ * wss.on('connection', handler)
91
+ *
92
+ * @example <caption>Authentication in onConnect</caption>
93
+ * const handler = wiring(controllers, async (socket, req, send) => {
94
+ * // Extract and verify JWT from cookies
95
+ * const token = req.headers.cookie?.match(/token=([^;]+)/)?.[1]
96
+ *
97
+ * try {
98
+ * const user = await verifyJWT(token)
99
+ *
100
+ * // Send welcome message
101
+ * send('welcome', { userId: user.id, name: user.name })
102
+ *
103
+ * return {
104
+ * embed: {
105
+ * userId: user.id,
106
+ * permissions: user.permissions,
107
+ * isAdmin: user.roles.includes('admin')
108
+ * },
109
+ * onReceive: (queryId, data, type) => {
110
+ * logActivity(user.id, type, data)
111
+ * },
112
+ * onDisconnect: () => {
113
+ * updateUserStatus(user.id, 'offline')
114
+ * }
115
+ * }
116
+ * } catch (err) {
117
+ * // Reject connection
118
+ * send('error', { message: 'Authentication failed' })
119
+ * socket.close(4001, 'Unauthorized')
120
+ * return null
121
+ * }
122
+ * })
123
+ *
124
+ * @example <caption>Rate Limiting Example</caption>
125
+ * const handler = wiring(controllers, (socket, req, send) => {
126
+ * const ip = req.socket.remoteAddress
127
+ * const rateLimit = createRateLimiter(ip, { max: 100, window: 60000 })
128
+ *
129
+ * return {
130
+ * embed: { ip, rateLimit },
131
+ * onReceive: (queryId, data, type) => {
132
+ * if (!rateLimit.check()) {
133
+ * throw new Error('Rate limit exceeded')
134
+ * }
135
+ * }
136
+ * }
137
+ * })
138
+ */
10
139
 
140
+ const replySecurity = require("../security/reply");
141
+ const socketOpen = require("../socket/open");
142
+ const socketReceive = require("../socket/receive");
143
+ const socketSend = require("../socket/send");
144
+ const makeid = require("../utils/genId");
145
+ const parseUserAgent = require("../utils/parseUserAgent");
146
+ const {
147
+ addClient,
148
+ removeClient,
149
+ updateClientEmbed,
150
+ updateClientSend,
151
+ updateClientAuth,
152
+ } = require("./broadcast");
11
153
 
154
+ /**
155
+ * Merge user-provided event handlers with default no-op handlers
156
+ *
157
+ * Ensures that all event handler properties exist with at least a no-op
158
+ * function, preventing null checks throughout the codebase.
159
+ *
160
+ * @param {Object} [events={}] - User-provided event handlers from onConnect
161
+ * @param {Object} [events.embed={}] - Custom values to embed in controller context
162
+ * @param {Function} [events.onReceive] - Called when a message is received
163
+ * @param {Function} [events.onSend] - Called when a message is sent
164
+ * @param {Function} [events.onError] - Called when an error occurs
165
+ * @param {Function} [events.onDisconnect] - Called when the client disconnects
166
+ * @returns {Object} Merged event handlers with defaults for any missing handlers
167
+ * @private
168
+ *
169
+ * @example
170
+ * // User returns partial handlers
171
+ * const userEvents = { embed: { userId: 123 }, onDisconnect: () => {} }
172
+ *
173
+ * const merged = defaultEvents(userEvents)
174
+ * // merged = {
175
+ * // embed: { userId: 123 },
176
+ * // onReceive: () => {}, // default no-op
177
+ * // onSend: () => {}, // default no-op
178
+ * // onError: console.error, // default error logger
179
+ * // onDisconnect: () => {} // user provided
180
+ * // }
181
+ */
12
182
  function defaultEvents(events = {}) {
13
- const fallBackEvents = {
14
- embed: {},
15
- onReceive: () => { },
16
- onSend: () => { },
17
- onError: (errSt) => console.error(errSt),
18
- onDisconnect: () => { },
19
- } // END fallBackEvents
20
- return Object.assign({}, fallBackEvents, events)
21
- } // END defaultEvents
22
-
23
- //=====================================================
24
- //============================================== wiring
25
- //=====================================================
26
-
27
- module.exports = function wiring(controllers, onConnect, fileTransfer) {
28
- onConnect = onConnect || (() => { });
29
- return function webSocketHandler(socket, req) {
30
-
31
- let send;
32
- let sentBufferAr = []
33
- const sentBufferFn = (...args) => {
34
- if (send) {
35
- send(...args)
36
- } else {
37
- sentBufferAr.push(args)
38
- }
39
- } // END sentBufferFn
40
-
41
- const clientId = makeid(20)
42
- const agent = parseUserAgent(req.headers['user-agent'])
43
-
44
- // Extract sessionId from cookies (set by outer framework)
45
- const sessionIdMatch = (req.headers.cookie || '').match(/(?:^|;\s*)sessionId=([^;]*)/)
46
- const sessionId = sessionIdMatch ? sessionIdMatch[1] : null
47
-
48
- const sharedValues = {
49
- socket, req, agent, send: (type, data, err) => sentBufferFn(false, type, data, err)
183
+ const fallBackEvents = {
184
+ /**
185
+ * Default embed - empty object
186
+ * @type {Object}
187
+ */
188
+ embed: {},
189
+
190
+ /**
191
+ * Default onReceive - no-op
192
+ * @type {Function}
193
+ */
194
+ onReceive: () => {},
195
+
196
+ /**
197
+ * Default onSend - no-op
198
+ * @type {Function}
199
+ */
200
+ onSend: () => {},
201
+
202
+ /**
203
+ * Default onError - logs to console
204
+ * @param {string} errSt - Error message
205
+ */
206
+ onError: (errSt) => console.error(errSt),
207
+
208
+ /**
209
+ * Default onDisconnect - no-op
210
+ * @type {Function}
211
+ */
212
+ onDisconnect: () => {},
213
+ };
214
+
215
+ return Object.assign({}, fallBackEvents, events);
216
+ }
217
+
218
+ /**
219
+ * Create a WebSocket connection handler for api-ape
220
+ *
221
+ * This factory function creates a handler that should be attached to a
222
+ * WebSocketServer's 'connection' event. It sets up the full api-ape
223
+ * pipeline for each connecting client.
224
+ *
225
+ * ## Handler Responsibilities
226
+ *
227
+ * 1. **Client Identification**: Generates unique clientId, extracts sessionId
228
+ * 2. **User-Agent Parsing**: Identifies browser, OS, device type
229
+ * 3. **Client Tracking**: Registers client in the broadcast system
230
+ * 4. **Lifecycle Management**: Calls onConnect, manages disconnect cleanup
231
+ * 5. **Message Pipeline**: Sets up receive/send handlers for the socket
232
+ * 6. **Security**: Validates origin, prevents replay attacks
233
+ *
234
+ * ## onConnect Callback
235
+ *
236
+ * The `onConnect` function is called with:
237
+ * - `socket`: The WebSocket instance
238
+ * - `req`: The HTTP upgrade request
239
+ * - `send`: Function to send messages to this client
240
+ *
241
+ * It can return (or resolve to) an object with:
242
+ * - `embed`: Object of values available in all controllers as `this.*`
243
+ * - `onReceive(queryId, data, type)`: Called for each incoming message
244
+ * - `onSend(data, type)`: Called for each outgoing message
245
+ * - `onError(errorString)`: Called when errors occur
246
+ * - `onDisconnect()`: Called when the client disconnects
247
+ *
248
+ * @param {Object} controllers - Loaded controller functions keyed by endpoint path
249
+ * @param {Function} [onConnect] - Async callback for connection setup
250
+ * @param {Object} [fileTransfer] - File transfer manager instance for binary data
251
+ * @param {Object} [options] - Additional options
252
+ * @param {Object} [options.authFramework] - Auth framework instance for authentication
253
+ * @param {Object} [options.authMiddleware] - Authorization middleware instance
254
+ * @returns {Function} WebSocket connection handler `(socket, req) => void`
255
+ *
256
+ * @example <caption>Minimal Setup</caption>
257
+ * const handler = wiring(controllers)
258
+ * wss.on('connection', handler)
259
+ *
260
+ * @example <caption>With Authentication</caption>
261
+ * const handler = wiring(controllers, async (socket, req, send) => {
262
+ * const user = await authenticateRequest(req)
263
+ * if (!user) {
264
+ * socket.close(4001, 'Unauthorized')
265
+ * return null
266
+ * }
267
+ *
268
+ * return {
269
+ * embed: { user, permissions: user.permissions },
270
+ * onDisconnect: () => logUserDisconnect(user.id)
271
+ * }
272
+ * }, fileTransferManager)
273
+ *
274
+ * @example <caption>With Message Logging</caption>
275
+ * const handler = wiring(controllers, (socket, req, send) => {
276
+ * const clientIp = req.socket.remoteAddress
277
+ *
278
+ * return {
279
+ * embed: { ip: clientIp },
280
+ * onReceive: (queryId, data, type) => {
281
+ * console.log(`[${clientIp}] Received ${type}:`, data)
282
+ * },
283
+ * onSend: (data, type) => {
284
+ * console.log(`[${clientIp}] Sent ${type}:`, data)
285
+ * },
286
+ * onError: (errString) => {
287
+ * console.error(`[${clientIp}] Error:`, errString)
288
+ * }
289
+ * }
290
+ * })
291
+ *
292
+ * @example <caption>Early Message Sending</caption>
293
+ * const handler = wiring(controllers, (socket, req, send) => {
294
+ * // Send messages before returning
295
+ * // These are buffered until the connection is fully set up
296
+ * send('server-info', { version: '1.0', time: Date.now() })
297
+ * send('motd', { message: 'Welcome to the server!' })
298
+ *
299
+ * return { embed: {} }
300
+ * })
301
+ */
302
+ module.exports = function wiring(controllers, onConnect, fileTransfer, options = {}) {
303
+ // Default onConnect to no-op if not provided
304
+ onConnect = onConnect || (() => {});
305
+
306
+ // Extract auth framework and middleware from options
307
+ const { authFramework = null, authMiddleware = null } = options;
308
+
309
+ /**
310
+ * WebSocket connection handler
311
+ *
312
+ * Called by WebSocketServer when a new client connects.
313
+ * Sets up the complete api-ape pipeline for this connection.
314
+ *
315
+ * @param {WebSocket} socket - The WebSocket instance for this connection
316
+ * @param {http.IncomingMessage} req - The HTTP upgrade request
317
+ */
318
+ return function webSocketHandler(socket, req) {
319
+ /**
320
+ * Send function reference - assigned after setup completes
321
+ * @type {Function|undefined}
322
+ */
323
+ let send;
324
+
325
+ /**
326
+ * Buffer for messages sent before send function is ready
327
+ * These are flushed once the connection is fully established
328
+ * @type {Array<Array<any>>}
329
+ */
330
+ let sentBufferAr = [];
331
+
332
+ /**
333
+ * Buffered send function
334
+ *
335
+ * If the send function isn't ready yet, buffer the message.
336
+ * Otherwise, pass through to the real send function.
337
+ *
338
+ * @param {...any} args - Arguments to pass to send
339
+ */
340
+ /* istanbul ignore next 7 - send buffer fallthrough, only hit if user stores ref and calls later */
341
+ const sentBufferFn = (...args) => {
342
+ if (send) {
343
+ send(...args);
344
+ } else {
345
+ sentBufferAr.push(args);
346
+ }
347
+ };
348
+
349
+ /**
350
+ * Generate unique client identifier
351
+ * Uses 20-character Crockford Base32 string for uniqueness
352
+ * @type {string}
353
+ */
354
+ const clientId = makeid(20);
355
+
356
+ /**
357
+ * Parse user-agent header for browser/OS/device detection
358
+ * @type {Object}
359
+ */
360
+ const agent = parseUserAgent(req.headers["user-agent"]);
361
+
362
+ /**
363
+ * Extract sessionId from cookies
364
+ *
365
+ * Looks for a cookie named 'sessionId' which may be set by
366
+ * the outer web framework (Express, Koa, etc.)
367
+ *
368
+ * @type {string|null}
369
+ */
370
+ const sessionIdMatch = (req.headers.cookie || "").match(
371
+ /(?:^|;\s*)sessionId=([^;]*)/,
372
+ );
373
+ const sessionId = sessionIdMatch ? sessionIdMatch[1] : null;
374
+
375
+ /**
376
+ * Shared values accessible in onConnect callback
377
+ *
378
+ * The send function's toString() returns the clientId for identification
379
+ *
380
+ * @type {Object}
381
+ */
382
+ const sharedValues = {
383
+ socket,
384
+ req,
385
+ agent,
386
+ send: (type, data, err) => sentBufferFn(false, type, data, err),
387
+ };
388
+
389
+ // Allow clientId to be retrieved from send function
390
+ sharedValues.send.toString = () => clientId;
391
+
392
+ /**
393
+ * Register client for broadcast BEFORE calling onConnect
394
+ *
395
+ * This ensures ape.clients.size returns the correct count
396
+ * when the onConnect callback executes and potentially sends
397
+ * initial messages.
398
+ */
399
+ addClient({ clientId, sessionId, agent, send: null, embed: null });
400
+
401
+ /**
402
+ * Set up disconnect handler early
403
+ *
404
+ * This will properly clean up the client even if onConnect
405
+ * fails or the connection closes during setup.
406
+ */
407
+ socket.on("close", () => {
408
+ removeClient(clientId);
409
+ });
410
+
411
+ /**
412
+ * Call onConnect and handle the result
413
+ *
414
+ * onConnect can be sync or async. We normalize to Promise
415
+ * and then process the returned event handlers.
416
+ */
417
+ let result = onConnect(socket, req, sharedValues.send);
418
+
419
+ // Normalize to Promise
420
+ if (!result || !result.then) {
421
+ result = Promise.resolve(result);
422
+ }
423
+
424
+ result
425
+ .then(defaultEvents)
426
+ .then(({ embed, onReceive, onSend, onError, onDisconnect }) => {
427
+ /**
428
+ * Validate connection security (origin check)
429
+ *
430
+ * If validation fails, the socket is destroyed and we clean up.
431
+ */
432
+ const isOk = socketOpen(socket, req, onError);
433
+
434
+ /* istanbul ignore next 4 - origin validation failure, requires CORS misconfiguration */
435
+ if (!isOk) {
436
+ removeClient(clientId); // Clean up if connection fails
437
+ return;
50
438
  }
51
- sharedValues.send.toString = () => clientId
52
439
 
53
- // Track this client for broadcast BEFORE calling onConnect
54
- // This ensures ape.clients.size returns the correct count when sending init
55
- addClient({ clientId, sessionId, agent, send: null, embed: null })
440
+ /**
441
+ * Create replay attack prevention checker
442
+ *
443
+ * This tracks recent queryIds and timestamps to prevent
444
+ * duplicate or stale requests from being processed.
445
+ */
446
+ const checkReply = replySecurity();
447
+
448
+ /**
449
+ * Create socket auth manager if auth framework is configured
450
+ *
451
+ * This tracks authentication state for this connection and
452
+ * handles auth message routing.
453
+ */
454
+ const socketAuth = authFramework
455
+ ? authFramework.createSocketAuth(clientId)
456
+ : null;
457
+
458
+ /**
459
+ * Ape context object
460
+ *
461
+ * Contains all the information needed by the socket handlers
462
+ * to process messages for this connection.
463
+ *
464
+ * @type {Object}
465
+ */
466
+ const ape = {
467
+ socket,
468
+ req,
469
+ clientId,
470
+ checkReply,
471
+ events: { onReceive, onSend, onError, onDisconnect },
472
+ controllers,
473
+ sharedValues,
474
+ embedValues: embed,
475
+ fileTransfer,
476
+ socketAuth,
477
+ authFramework,
478
+ authMiddleware,
479
+ };
480
+
481
+ /**
482
+ * Create the send handler for this connection
483
+ * @type {Function}
484
+ */
485
+ send = socketSend(ape);
486
+ ape.send = send;
56
487
 
57
- // Remove client on disconnect (set up early, will work once send is assigned)
58
- socket.on('close', () => {
59
- removeClient(clientId)
60
- })
488
+ /**
489
+ * Update client record with actual send function and embed values
490
+ *
491
+ * Now that setup is complete, the client can receive messages
492
+ * and the embed values are available for querying.
493
+ */
494
+ updateClientSend(clientId, send);
495
+ updateClientEmbed(clientId, embed);
61
496
 
62
- let result = onConnect(socket, req, sharedValues.send)
63
- if (!result || !result.then) {
64
- result = Promise.resolve(result)
497
+ /**
498
+ * Update client record with auth state manager
499
+ *
500
+ * This allows querying auth state for any connected client
501
+ * via ape.clients.get(clientId).authState
502
+ */
503
+ if (socketAuth) {
504
+ updateClientAuth(clientId, socketAuth);
65
505
  }
66
- result.then(defaultEvents)
67
- .then(({ embed, onReceive, onSend, onError, onDisconnect }) => {
68
- const isOk = socketOpen(socket, req, onError)
69
-
70
- if (!isOk) {
71
- removeClient(clientId) // Clean up if connection fails
72
- return;
73
- }
74
-
75
-
76
- const checkReply = replySecurity()
77
- const ape = {
78
- socket,
79
- req,
80
- clientId,
81
- checkReply,
82
- events: { onReceive, onSend, onError, onDisconnect },
83
- controllers,
84
- sharedValues,
85
- embedValues: embed,
86
- fileTransfer // Pass file transfer manager
87
- }// END ape
88
- send = socketSend(ape)
89
- ape.send = send
90
-
91
- // Update client with real send function and embed values
92
- updateClientSend(clientId, send)
93
- updateClientEmbed(clientId, embed)
94
-
95
- // Call onDisconnect when socket closes
96
- socket.on('close', () => {
97
- onDisconnect()
98
- })
99
-
100
- sentBufferAr.forEach(args => send(...args))
101
- sentBufferAr = []
102
- socket.on('message', socketReceive(ape))
103
- }) // END result.then
104
-
105
- } // END webSocketHandler
106
- } // END wiring
107
506
 
507
+ /**
508
+ * Register disconnect handler with user callback
509
+ *
510
+ * When the socket closes, call the user's onDisconnect handler.
511
+ * The removeClient call was already set up above.
512
+ */
513
+ socket.on("close", () => {
514
+ // Clean up auth resources
515
+ if (socketAuth) {
516
+ socketAuth.cleanup();
517
+ }
518
+ onDisconnect();
519
+ });
520
+
521
+ /**
522
+ * Send connection acknowledgment with clientId
523
+ *
524
+ * This allows WebSocket clients to know their clientId for use
525
+ * in HTTP requests (e.g., binary file uploads via PUT).
526
+ */
527
+ send(null, "__connected__", { clientId }, null);
528
+
529
+ /**
530
+ * Flush any messages that were buffered during setup
531
+ *
532
+ * These are typically messages sent from within onConnect
533
+ * before the send function was fully initialized.
534
+ */
535
+ sentBufferAr.forEach((args) => send(...args));
536
+ sentBufferAr = [];
537
+
538
+ /**
539
+ * Attach the message handler
540
+ *
541
+ * All incoming WebSocket messages will be processed by
542
+ * the socketReceive handler, which routes them to the
543
+ * appropriate controller.
544
+ */
545
+ socket.on("message", socketReceive(ape));
546
+ });
547
+ };
548
+ };
@@ -0,0 +1,35 @@
1
+ # WebSocket Polyfill Module
2
+
3
+ ## Overview
4
+
5
+ The ws module provides a zero-dependency, RFC 6455 compliant WebSocket implementation for api-ape. It serves as a fallback when native WebSocket support is unavailable, ensuring api-ape works across all Node.js versions without requiring external packages.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **RFC 6455 compliant** — Full implementation of the WebSocket protocol specification
10
+ - **Zero dependencies** — Pure JavaScript implementation with no external packages
11
+ - **ws library compatible** — Drop-in replacement matching the popular `ws` package API
12
+ - **Frame protocol** — Complete frame encoding, decoding, masking, and fragmentation support
13
+ - **Control frames** — Proper handling of ping, pong, and close frames
14
+ - **Runtime adapters** — Adapters for Bun and Deno native WebSocket implementations
15
+
16
+ The polyfill is automatically selected by `wsProvider.js` when running on Node.js versions prior to 24 (which introduced native WebSocket support) and when native runtime adapters are unavailable.
17
+
18
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
19
+
20
+ ## When Is This Used?
21
+
22
+ The polyfill is automatically selected by `wsProvider.js` when:
23
+
24
+ | Condition | WebSocket Provider Used |
25
+ |-----------|------------------------|
26
+ | Deno runtime | Native `Deno.upgradeWebSocket()` |
27
+ | Bun runtime | Native Bun WebSocket |
28
+ | Node.js 24+ | Native `node:ws` module |
29
+ | Node.js < 24 | **This polyfill** |
30
+
31
+ ## See Also
32
+
33
+ - [`adapters/README.md`](./adapters/README.md) — Runtime-specific WebSocket adapters
34
+ - [`../wsProvider.js`](../wsProvider.js) — Runtime detection and provider selection
35
+ - [RFC 6455](https://tools.ietf.org/html/rfc6455) — WebSocket Protocol specification