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.
- package/README.md +58 -570
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -202
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +32 -7
- package/server/README.md +287 -53
- package/server/adapters/README.md +28 -19
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +332 -27
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -221
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -225
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -308
- package/server/lib/broadcast.js +0 -146
package/server/lib/wiring.js
CHANGED
|
@@ -1,107 +1,548 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|