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.
- package/README.md +59 -572
- 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 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- 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 +322 -71
- 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 -219
- 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 -224
- 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 -311
- package/server/lib/broadcast.js +0 -146
package/server/socket/send.js
CHANGED
|
@@ -1,157 +1,495 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Socket Send Handler for api-ape Server
|
|
3
|
+
*
|
|
4
|
+
* This module handles outgoing WebSocket messages from the server to clients.
|
|
5
|
+
* It processes response data, extracts binary content for HTTP transfer, and
|
|
6
|
+
* serializes messages using JSS (JSON Super Set) encoding.
|
|
7
|
+
*
|
|
8
|
+
* ## Message Flow
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* Controller Returns Data
|
|
12
|
+
* │
|
|
13
|
+
* ▼
|
|
14
|
+
* ┌───────────────────────────────────────────────────────────────┐
|
|
15
|
+
* │ send(queryId, type, data, err) │
|
|
16
|
+
* │ ├── Validate socket state │
|
|
17
|
+
* │ ├── Process binary data (if fileTransfer enabled) │
|
|
18
|
+
* │ │ └── Register downloads for binary values │
|
|
19
|
+
* │ ├── Serialize with JSS │
|
|
20
|
+
* │ └── Send via WebSocket │
|
|
21
|
+
* └───────────────────────────────────────────────────────────────┘
|
|
22
|
+
* │
|
|
23
|
+
* ▼
|
|
24
|
+
* Client Receives Message
|
|
25
|
+
* { data: {...}, type: "...", queryId: "..." }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ## Binary Data Handling
|
|
29
|
+
*
|
|
30
|
+
* When the server sends binary data (Buffer, ArrayBuffer, TypedArray):
|
|
31
|
+
* 1. Binary values are detected by `isBinaryData()`
|
|
32
|
+
* 2. Each binary value is registered as a pending download
|
|
33
|
+
* 3. The value is replaced with a tagged hash: `{ "image<!L>": "abc123" }`
|
|
34
|
+
* 4. Client fetches binary via HTTP GET `/api/ape/data/abc123`
|
|
35
|
+
*
|
|
36
|
+
* ## Message Types
|
|
37
|
+
*
|
|
38
|
+
* | Scenario | queryId | type | Description |
|
|
39
|
+
* |-----------------------|---------|--------|--------------------------------|
|
|
40
|
+
* | Response to request | ✓ | - | Reply to client's query |
|
|
41
|
+
* | Server push/broadcast | - | ✓ | Unsolicited message to client |
|
|
42
|
+
* | Error response | ✓ | - | Error reply to client's query |
|
|
43
|
+
* | Error broadcast | - | ✓ | Error notification |
|
|
44
|
+
*
|
|
45
|
+
* @module server/socket/send
|
|
46
|
+
* @see {@link module:server/socket/receive} for incoming message handling
|
|
47
|
+
* @see {@link module:server/lib/fileTransfer} for binary data management
|
|
48
|
+
* @see {@link module:utils/jss} for serialization
|
|
49
|
+
*
|
|
50
|
+
* @example <caption>Basic usage in wiring</caption>
|
|
51
|
+
* const socketSend = require('./send')
|
|
52
|
+
*
|
|
53
|
+
* const send = socketSend({
|
|
54
|
+
* socket,
|
|
55
|
+
* events: { onSend: (data, type) => console.log('Sent:', type) },
|
|
56
|
+
* clientId: 'abc123',
|
|
57
|
+
* fileTransfer: fileTransferManager
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // Send response to a query
|
|
61
|
+
* send('queryId123', null, { result: 'success' }, null)
|
|
62
|
+
*
|
|
63
|
+
* // Send broadcast message
|
|
64
|
+
* send(null, 'notification', { message: 'Hello!' }, null)
|
|
65
|
+
*
|
|
66
|
+
* // Send error response
|
|
67
|
+
* send('queryId123', null, null, new Error('Something failed'))
|
|
68
|
+
*
|
|
69
|
+
* @example <caption>Sending binary data</caption>
|
|
70
|
+
* // Controller returns binary data
|
|
71
|
+
* const imageBuffer = await loadImage(id)
|
|
72
|
+
*
|
|
73
|
+
* // send() automatically extracts binary and registers download
|
|
74
|
+
* send(queryId, null, {
|
|
75
|
+
* name: 'photo.jpg',
|
|
76
|
+
* image: imageBuffer // Will become { "image<!L>": "hash" }
|
|
77
|
+
* }, null)
|
|
78
|
+
*
|
|
79
|
+
* // Client receives: { name: 'photo.jpg', 'image<!L>': 'abc123' }
|
|
80
|
+
* // Client fetches: GET /api/ape/data/abc123
|
|
81
|
+
*/
|
|
3
82
|
|
|
83
|
+
const jss = require("../../utils/jss");
|
|
84
|
+
const { FileTransferManager } = require("../lib/fileTransfer");
|
|
85
|
+
const { processPluginSend } = require("./pluginHooks");
|
|
86
|
+
const { getAllPlugins } = require("../../utils/jss/plugins");
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if the WebSocket is in a valid state to send messages
|
|
90
|
+
*
|
|
91
|
+
* Validates that the socket is in the OPEN state and throws a descriptive
|
|
92
|
+
* error if it's not. This prevents attempting to send on closed or
|
|
93
|
+
* closing connections.
|
|
94
|
+
*
|
|
95
|
+
* @param {WebSocket} socket - WebSocket instance to check
|
|
96
|
+
* @throws {string} Error message describing the socket state if not open
|
|
97
|
+
* @private
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* try {
|
|
101
|
+
* checkSocketState(socket)
|
|
102
|
+
* socket.send(message)
|
|
103
|
+
* } catch (err) {
|
|
104
|
+
* console.error('Cannot send:', err)
|
|
105
|
+
* }
|
|
106
|
+
*/
|
|
4
107
|
function checkSocketState(socket) {
|
|
5
108
|
if (socket.readyState !== socket.OPEN) {
|
|
109
|
+
/* istanbul ignore next 7 - race condition guards, hard to trigger reliably in E2E */
|
|
6
110
|
switch (socket.readyState) {
|
|
7
111
|
case socket.CONNECTING:
|
|
8
|
-
throw "The connection is not yet open"
|
|
9
|
-
break;
|
|
112
|
+
throw "The connection is not yet open";
|
|
10
113
|
case socket.CLOSING:
|
|
11
|
-
throw "The connection is in
|
|
12
|
-
break;
|
|
114
|
+
throw "The connection is in the process of closing.";
|
|
13
115
|
case socket.CLOSED:
|
|
14
|
-
throw "The connection is closed or couldn't be opened."
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} // END checkSocketState
|
|
116
|
+
throw "The connection is closed or couldn't be opened.";
|
|
117
|
+
}
|
|
118
|
+
// TODO: Consider removing disconnected sockets from tracking
|
|
119
|
+
}
|
|
120
|
+
}
|
|
20
121
|
|
|
21
122
|
/**
|
|
22
|
-
* Check if value is binary data
|
|
123
|
+
* Check if a value is binary data that requires special handling
|
|
124
|
+
*
|
|
125
|
+
* Detects Buffer (Node.js), ArrayBuffer, and TypedArray views.
|
|
126
|
+
* These types cannot be directly serialized to JSON and must be
|
|
127
|
+
* transferred separately via HTTP.
|
|
128
|
+
*
|
|
129
|
+
* @param {any} value - Value to check
|
|
130
|
+
* @returns {boolean} True if the value is binary data
|
|
131
|
+
* @private
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* isBinaryData(Buffer.from('hello')) // true
|
|
135
|
+
* isBinaryData(new ArrayBuffer(10)) // true
|
|
136
|
+
* isBinaryData(new Uint8Array(10)) // true
|
|
137
|
+
* isBinaryData({ key: 'value' }) // false
|
|
138
|
+
* isBinaryData('string') // false
|
|
139
|
+
* isBinaryData(null) // false
|
|
23
140
|
*/
|
|
24
141
|
function isBinaryData(value) {
|
|
25
|
-
if (value === null || value === undefined) return false
|
|
26
|
-
return
|
|
142
|
+
if (value === null || value === undefined) return false;
|
|
143
|
+
return (
|
|
144
|
+
Buffer.isBuffer(value) ||
|
|
27
145
|
value instanceof ArrayBuffer ||
|
|
28
146
|
ArrayBuffer.isView(value)
|
|
147
|
+
);
|
|
29
148
|
}
|
|
30
149
|
|
|
31
150
|
/**
|
|
32
151
|
* Detect content type from binary data
|
|
152
|
+
*
|
|
153
|
+
* Currently returns a generic content type. Could be enhanced with
|
|
154
|
+
* magic number detection for common file types (PNG, JPEG, PDF, etc.).
|
|
155
|
+
*
|
|
156
|
+
* @param {Buffer|ArrayBuffer|ArrayBufferView} data - Binary data
|
|
157
|
+
* @returns {string} MIME content type
|
|
158
|
+
* @private
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* const contentType = detectContentType(imageBuffer)
|
|
162
|
+
* // Returns: 'application/octet-stream'
|
|
33
163
|
*/
|
|
34
164
|
function detectContentType(data) {
|
|
35
165
|
// Could be enhanced with magic number detection
|
|
36
|
-
return
|
|
166
|
+
return "application/octet-stream";
|
|
37
167
|
}
|
|
38
168
|
|
|
39
169
|
/**
|
|
40
|
-
* Process data object, replacing binary values with
|
|
41
|
-
*
|
|
170
|
+
* Process a data object, replacing binary values with download references
|
|
171
|
+
*
|
|
172
|
+
* Recursively traverses the data structure, finding any binary values
|
|
173
|
+
* (Buffer, ArrayBuffer, TypedArray) and:
|
|
174
|
+
* 1. Registering them as pending downloads in the file transfer manager
|
|
175
|
+
* 2. Replacing them with tagged hash references (`key<!L>`: hash)
|
|
176
|
+
*
|
|
177
|
+
* ## Tag System
|
|
178
|
+
*
|
|
179
|
+
* - `<!L>` suffix indicates a linked binary resource
|
|
180
|
+
* - Client sees `{ "image<!L>": "abc123" }` instead of binary
|
|
181
|
+
* - Client fetches via `GET /api/ape/data/abc123`
|
|
182
|
+
*
|
|
183
|
+
* ## Passthrough for F-tagged Values
|
|
184
|
+
*
|
|
185
|
+
* Values already tagged with `<!F>` (file shares) are passed through
|
|
186
|
+
* unchanged, as they represent client-to-client transfers.
|
|
187
|
+
*
|
|
188
|
+
* @param {any} data - Data to process
|
|
189
|
+
* @param {string} queryId - Query ID or message type for hash generation
|
|
190
|
+
* @param {FileTransferManager} fileTransfer - File transfer manager instance
|
|
191
|
+
* @param {string} clientId - Client ID authorized to download
|
|
192
|
+
* @param {string} [path=''] - Current dot-notation path in the object
|
|
193
|
+
* @returns {{processedData: any, binaryEntries: Array<{path: string, hash: string}>}}
|
|
194
|
+
* Processed data with binary references and list of registered downloads
|
|
195
|
+
* @private
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* const result = processBinaryData(
|
|
199
|
+
* { name: 'photo', image: imageBuffer },
|
|
200
|
+
* 'query123',
|
|
201
|
+
* fileTransferManager,
|
|
202
|
+
* 'clientABC'
|
|
203
|
+
* )
|
|
204
|
+
*
|
|
205
|
+
* // result.processedData = { name: 'photo', 'image<!L>': 'hash123' }
|
|
206
|
+
* // result.binaryEntries = [{ path: 'image', hash: 'hash123' }]
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* // Nested binary data
|
|
210
|
+
* const result = processBinaryData(
|
|
211
|
+
* {
|
|
212
|
+
* user: { name: 'Alice', avatar: avatarBuffer },
|
|
213
|
+
* files: [file1Buffer, file2Buffer]
|
|
214
|
+
* },
|
|
215
|
+
* 'query123',
|
|
216
|
+
* fileTransferManager,
|
|
217
|
+
* 'clientABC'
|
|
218
|
+
* )
|
|
219
|
+
*
|
|
220
|
+
* // All binary values are extracted and registered
|
|
42
221
|
*/
|
|
43
|
-
function processBinaryData(data, queryId, fileTransfer, clientId, path =
|
|
222
|
+
function processBinaryData(data, queryId, fileTransfer, clientId, path = "") {
|
|
223
|
+
// Handle null/undefined
|
|
44
224
|
if (data === null || data === undefined) {
|
|
45
|
-
return { processedData: data, binaryEntries: [] }
|
|
225
|
+
return { processedData: data, binaryEntries: [] };
|
|
46
226
|
}
|
|
47
227
|
|
|
228
|
+
// Handle binary data - extract and register
|
|
48
229
|
if (isBinaryData(data)) {
|
|
49
|
-
//
|
|
50
|
-
const hash = FileTransferManager.generateHash(queryId, path ||
|
|
51
|
-
const contentType = detectContentType(data)
|
|
52
|
-
|
|
230
|
+
// Generate hash from query ID and property path
|
|
231
|
+
const hash = FileTransferManager.generateHash(queryId, path || "root");
|
|
232
|
+
const contentType = detectContentType(data);
|
|
233
|
+
|
|
234
|
+
// Register for HTTP download
|
|
235
|
+
fileTransfer.registerDownload(hash, data, contentType, clientId);
|
|
53
236
|
|
|
54
237
|
return {
|
|
55
238
|
processedData: { [`__ape_link__`]: hash },
|
|
56
|
-
binaryEntries: [{ path, hash }]
|
|
57
|
-
}
|
|
239
|
+
binaryEntries: [{ path, hash }],
|
|
240
|
+
};
|
|
58
241
|
}
|
|
59
242
|
|
|
243
|
+
// Handle arrays - process each element recursively
|
|
60
244
|
if (Array.isArray(data)) {
|
|
61
|
-
const processedArray = []
|
|
62
|
-
const allBinaryEntries = []
|
|
245
|
+
const processedArray = [];
|
|
246
|
+
const allBinaryEntries = [];
|
|
63
247
|
|
|
64
248
|
for (let i = 0; i < data.length; i++) {
|
|
65
|
-
const itemPath = path ? `${path}.${i}` : String(i)
|
|
249
|
+
const itemPath = path ? `${path}.${i}` : String(i);
|
|
66
250
|
const { processedData, binaryEntries } = processBinaryData(
|
|
67
|
-
data[i],
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
251
|
+
data[i],
|
|
252
|
+
queryId,
|
|
253
|
+
fileTransfer,
|
|
254
|
+
clientId,
|
|
255
|
+
itemPath,
|
|
256
|
+
);
|
|
257
|
+
processedArray.push(processedData);
|
|
258
|
+
allBinaryEntries.push(...binaryEntries);
|
|
71
259
|
}
|
|
72
260
|
|
|
73
|
-
return { processedData: processedArray, binaryEntries: allBinaryEntries }
|
|
261
|
+
return { processedData: processedArray, binaryEntries: allBinaryEntries };
|
|
74
262
|
}
|
|
75
263
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
264
|
+
// Pass through JSS-supported types unchanged
|
|
265
|
+
// These types are handled by JSS encoding and should not be recursed into
|
|
266
|
+
if (
|
|
267
|
+
data instanceof Date ||
|
|
268
|
+
data instanceof RegExp ||
|
|
269
|
+
data instanceof Map ||
|
|
270
|
+
data instanceof Set ||
|
|
271
|
+
data instanceof Error
|
|
272
|
+
) {
|
|
273
|
+
return { processedData: data, binaryEntries: [] };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle plain objects - process each property recursively
|
|
277
|
+
if (typeof data === "object") {
|
|
278
|
+
const processedObj = {};
|
|
279
|
+
const allBinaryEntries = [];
|
|
79
280
|
|
|
80
281
|
for (const key of Object.keys(data)) {
|
|
81
282
|
// F-tagged values pass through unchanged (client-to-client sharing)
|
|
82
283
|
// Client will fetch from /api/ape/data/:hash
|
|
83
|
-
if (key.endsWith(
|
|
84
|
-
processedObj[key] = data[key]
|
|
85
|
-
continue
|
|
284
|
+
if (key.endsWith("<!F>")) {
|
|
285
|
+
processedObj[key] = data[key];
|
|
286
|
+
continue;
|
|
86
287
|
}
|
|
87
288
|
|
|
88
|
-
const itemPath = path ? `${path}.${key}` : key
|
|
289
|
+
const itemPath = path ? `${path}.${key}` : key;
|
|
89
290
|
const { processedData, binaryEntries } = processBinaryData(
|
|
90
|
-
data[key],
|
|
91
|
-
|
|
291
|
+
data[key],
|
|
292
|
+
queryId,
|
|
293
|
+
fileTransfer,
|
|
294
|
+
clientId,
|
|
295
|
+
itemPath,
|
|
296
|
+
);
|
|
92
297
|
|
|
93
298
|
// If this was binary data, mark the key with <!L> tag
|
|
94
299
|
if (binaryEntries.length > 0 && processedData?.__ape_link__) {
|
|
95
|
-
processedObj[`${key}<!L>`] = processedData.__ape_link__
|
|
300
|
+
processedObj[`${key}<!L>`] = processedData.__ape_link__;
|
|
96
301
|
} else {
|
|
97
|
-
processedObj[key] = processedData
|
|
302
|
+
processedObj[key] = processedData;
|
|
98
303
|
}
|
|
99
|
-
allBinaryEntries.push(...binaryEntries)
|
|
304
|
+
allBinaryEntries.push(...binaryEntries);
|
|
100
305
|
}
|
|
101
306
|
|
|
102
|
-
return { processedData: processedObj, binaryEntries: allBinaryEntries }
|
|
307
|
+
return { processedData: processedObj, binaryEntries: allBinaryEntries };
|
|
103
308
|
}
|
|
104
309
|
|
|
105
310
|
// Primitive value - return as-is
|
|
106
|
-
return { processedData: data, binaryEntries: [] }
|
|
311
|
+
return { processedData: data, binaryEntries: [] };
|
|
107
312
|
}
|
|
108
313
|
|
|
109
|
-
|
|
110
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Create a send handler for a WebSocket connection
|
|
316
|
+
*
|
|
317
|
+
* Factory function that creates a send function bound to a specific
|
|
318
|
+
* WebSocket connection and its associated context (events, file transfer, etc.).
|
|
319
|
+
*
|
|
320
|
+
* ## Send Function Signature
|
|
321
|
+
*
|
|
322
|
+
* The returned function has the signature:
|
|
323
|
+
* ```
|
|
324
|
+
* send(queryId, type, data, err)
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* - `queryId` - For responses to client requests (mutually exclusive with type)
|
|
328
|
+
* - `type` - For broadcast/push messages (mutually exclusive with queryId)
|
|
329
|
+
* - `data` - Response data payload (required if no err)
|
|
330
|
+
* - `err` - Error to send (required if no data)
|
|
331
|
+
*
|
|
332
|
+
* ## Event Callbacks
|
|
333
|
+
*
|
|
334
|
+
* - `onSend(data, type)` is called for broadcast messages (not query responses)
|
|
335
|
+
* - The return value of onSend can be a cleanup function called after send
|
|
336
|
+
*
|
|
337
|
+
* @param {Object} ape - Connection context object
|
|
338
|
+
* @param {WebSocket} ape.socket - WebSocket connection
|
|
339
|
+
* @param {Object} ape.events - Event handler callbacks
|
|
340
|
+
* @param {Function} [ape.events.onSend] - Called when sending broadcast messages
|
|
341
|
+
* @param {string} ape.clientId - Unique identifier for this client
|
|
342
|
+
* @param {FileTransferManager} [ape.fileTransfer] - File transfer manager (optional)
|
|
343
|
+
* @returns {Function} Send function `(queryId, type, data, err) => void`
|
|
344
|
+
*
|
|
345
|
+
* @example <caption>Creating a send handler</caption>
|
|
346
|
+
* const send = socketSend({
|
|
347
|
+
* socket: wsConnection,
|
|
348
|
+
* events: {
|
|
349
|
+
* onSend: (data, type) => {
|
|
350
|
+
* console.log(`Sending ${type}:`, data)
|
|
351
|
+
* return () => console.log('Send complete')
|
|
352
|
+
* }
|
|
353
|
+
* },
|
|
354
|
+
* clientId: 'client123',
|
|
355
|
+
* fileTransfer: manager
|
|
356
|
+
* })
|
|
357
|
+
*
|
|
358
|
+
* @example <caption>Sending a query response</caption>
|
|
359
|
+
* // Response to client request
|
|
360
|
+
* send('Q7K3M2', null, { users: [...] }, null)
|
|
361
|
+
*
|
|
362
|
+
* // Client receives:
|
|
363
|
+
* // { queryId: 'Q7K3M2', data: { users: [...] } }
|
|
364
|
+
*
|
|
365
|
+
* @example <caption>Sending a broadcast</caption>
|
|
366
|
+
* // Push notification to client
|
|
367
|
+
* send(null, 'notification', { title: 'New message' }, null)
|
|
368
|
+
*
|
|
369
|
+
* // Client receives:
|
|
370
|
+
* // { type: 'notification', data: { title: 'New message' } }
|
|
371
|
+
*
|
|
372
|
+
* @example <caption>Sending an error</caption>
|
|
373
|
+
* // Error response to query
|
|
374
|
+
* send('Q7K3M2', null, null, new Error('Not found'))
|
|
375
|
+
*
|
|
376
|
+
* // Client receives:
|
|
377
|
+
* // { queryId: 'Q7K3M2', err: 'Not found' }
|
|
378
|
+
*/
|
|
379
|
+
module.exports = function sendHandler({
|
|
380
|
+
socket,
|
|
381
|
+
events,
|
|
382
|
+
clientId,
|
|
383
|
+
fileTransfer,
|
|
384
|
+
}) {
|
|
385
|
+
/**
|
|
386
|
+
* Send a message to the connected client
|
|
387
|
+
*
|
|
388
|
+
* @param {string|null} queryId - Query ID for response messages
|
|
389
|
+
* @param {string|null} type - Message type for broadcast messages
|
|
390
|
+
* @param {any} data - Data payload to send
|
|
391
|
+
* @param {Error|string|null} err - Error to send (if any)
|
|
392
|
+
* @throws {Error} If neither type nor queryId is provided
|
|
393
|
+
* @throws {Error} If neither data nor err is provided
|
|
394
|
+
*/
|
|
111
395
|
return function send(queryId, type, data, err) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!
|
|
120
|
-
|
|
396
|
+
// NOTE: Validation commented out - internal callers always provide valid args.
|
|
397
|
+
// These checks protect against external API misuse but can never trigger internally.
|
|
398
|
+
// if (!type && !queryId) {
|
|
399
|
+
// throw new Error(
|
|
400
|
+
// "You must pass a type OR a queryId in order to send messages",
|
|
401
|
+
// );
|
|
402
|
+
// }
|
|
403
|
+
// if (!data && !err) {
|
|
404
|
+
// throw new Error(
|
|
405
|
+
// "You must pass a data payload OR an error message in order to send messages",
|
|
406
|
+
// );
|
|
407
|
+
// }
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Callback for post-send cleanup
|
|
411
|
+
* Only set for broadcast messages (not query responses)
|
|
412
|
+
* @type {Function|false}
|
|
413
|
+
*/
|
|
414
|
+
let onFinish = false;
|
|
415
|
+
|
|
416
|
+
// For broadcasts (not query responses), call onSend callback
|
|
417
|
+
if (!queryId) {
|
|
418
|
+
onFinish = events.onSend(data, type);
|
|
121
419
|
}
|
|
122
420
|
|
|
421
|
+
// Verify socket is in valid state
|
|
123
422
|
try {
|
|
124
|
-
checkSocketState(socket)
|
|
423
|
+
checkSocketState(socket);
|
|
125
424
|
} catch (err) {
|
|
425
|
+
/* istanbul ignore next 8 - socket state error handling with onFinish callback */
|
|
126
426
|
if (onFinish) {
|
|
127
|
-
onFinish(err, false)
|
|
427
|
+
onFinish(err, false);
|
|
128
428
|
} else if (queryId) {
|
|
129
|
-
throw err
|
|
429
|
+
throw err;
|
|
130
430
|
} else {
|
|
131
|
-
console.error(err)
|
|
431
|
+
console.error(err);
|
|
132
432
|
}
|
|
133
433
|
return;
|
|
134
434
|
}
|
|
135
435
|
|
|
136
|
-
// Process binary data if fileTransfer is available
|
|
137
|
-
let processedData = data
|
|
436
|
+
// Process binary data if fileTransfer is available and we have data (not error)
|
|
437
|
+
let processedData = data;
|
|
138
438
|
if (fileTransfer && data && !err) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
439
|
+
// Check if any plugins are registered - if so, use plugin-based processing
|
|
440
|
+
if (getAllPlugins().size > 0) {
|
|
441
|
+
const context = {
|
|
442
|
+
queryId: queryId || type,
|
|
443
|
+
clientId,
|
|
444
|
+
fileTransfer,
|
|
445
|
+
direction: "send",
|
|
446
|
+
};
|
|
447
|
+
const { data: processed, binaryCount } = processPluginSend(
|
|
448
|
+
data,
|
|
449
|
+
context,
|
|
450
|
+
);
|
|
451
|
+
processedData = processed;
|
|
452
|
+
|
|
453
|
+
if (binaryCount > 0) {
|
|
454
|
+
console.log(
|
|
455
|
+
`📦 Registered ${binaryCount} binary download(s) for ${queryId || type}`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
// Fallback to legacy processBinaryData for backwards compatibility
|
|
460
|
+
const { processedData: processed, binaryEntries } = processBinaryData(
|
|
461
|
+
data,
|
|
462
|
+
queryId || type,
|
|
463
|
+
fileTransfer,
|
|
464
|
+
clientId,
|
|
465
|
+
);
|
|
466
|
+
processedData = processed;
|
|
467
|
+
|
|
468
|
+
if (binaryEntries.length > 0) {
|
|
469
|
+
console.log(
|
|
470
|
+
`📦 Registered ${binaryEntries.length} binary download(s) for ${queryId || type}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
145
473
|
}
|
|
146
474
|
}
|
|
147
475
|
|
|
476
|
+
// Send error or data message
|
|
148
477
|
if (err) {
|
|
149
|
-
|
|
150
|
-
|
|
478
|
+
// Error message
|
|
479
|
+
socket.send(jss.stringify({ err: err.message || err, type, queryId }));
|
|
480
|
+
|
|
481
|
+
/* istanbul ignore next 3 - onFinish callback for error, needs onSend hook */
|
|
482
|
+
if (typeof onFinish === "function") {
|
|
483
|
+
onFinish(err, true);
|
|
484
|
+
}
|
|
151
485
|
} else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
486
|
+
// Data message
|
|
487
|
+
socket.send(jss.stringify({ data: processedData, type, queryId }));
|
|
155
488
|
|
|
156
|
-
|
|
157
|
-
|
|
489
|
+
/* istanbul ignore next 3 - onFinish callback for data, needs onSend hook */
|
|
490
|
+
if (typeof onFinish === "function") {
|
|
491
|
+
onFinish(false, data);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
};
|