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
@@ -0,0 +1,346 @@
1
+ /**
2
+ * @fileoverview Common utilities for file handling in api-ape client
3
+ *
4
+ * This module provides shared utility functions used by both file upload
5
+ * (fileHandling.js) and file download (fileDownload.js) modules.
6
+ *
7
+ * ## Key Functions
8
+ *
9
+ * - **Binary Detection**: `isBinaryData()`, `getBinaryTag()`
10
+ * - **Hash Generation**: `generateUploadHash()`
11
+ * - **Object Traversal**: `setValueAtPath()`, `findTaggedProps()`, `cleanTaggedKeys()`
12
+ *
13
+ * ## Tag System
14
+ *
15
+ * api-ape uses special key suffixes to mark binary data references:
16
+ * - `<!L>` - Server-linked binary (download from server)
17
+ * - `<!F>` - File share (client-to-client transfer)
18
+ * - `<!A>` - ArrayBuffer upload
19
+ * - `<!B>` - Blob upload
20
+ *
21
+ * @module client/connection/fileUtils
22
+ * @see {@link module:client/connection/fileHandling} for upload processing
23
+ * @see {@link module:client/connection/fileDownload} for download processing
24
+ *
25
+ * @example
26
+ * import {
27
+ * isBinaryData,
28
+ * findTaggedProps,
29
+ * setValueAtPath
30
+ * } from './fileUtils'
31
+ *
32
+ * // Check for binary data
33
+ * const data = new ArrayBuffer(100)
34
+ * console.log(isBinaryData(data)) // true
35
+ *
36
+ * // Find tagged properties in response
37
+ * const response = { 'image<!L>': 'abc123', name: 'photo.jpg' }
38
+ * const tags = findTaggedProps(response, 'L')
39
+ * // [{ path: 'image', hash: 'abc123', originalKey: 'image<!L>' }]
40
+ */
41
+
42
+ /**
43
+ * Check if a value is binary data
44
+ *
45
+ * Detects ArrayBuffer, TypedArray views (Uint8Array, Int32Array, etc.),
46
+ * and Blob objects. Used to determine if a value needs special handling
47
+ * during serialization.
48
+ *
49
+ * @param {any} value - The value to check
50
+ * @returns {boolean} True if the value is binary data, false otherwise
51
+ *
52
+ * @example
53
+ * // ArrayBuffer
54
+ * isBinaryData(new ArrayBuffer(10)) // true
55
+ *
56
+ * // TypedArray views
57
+ * isBinaryData(new Uint8Array(10)) // true
58
+ * isBinaryData(new Float32Array(10)) // true
59
+ *
60
+ * // Blob (browser only)
61
+ * isBinaryData(new Blob(['hello'])) // true
62
+ *
63
+ * // Non-binary
64
+ * isBinaryData('string') // false
65
+ * isBinaryData({ key: 'value' }) // false
66
+ * isBinaryData(null) // false
67
+ * isBinaryData(undefined) // false
68
+ */
69
+ export function isBinaryData(value) {
70
+ if (value === null || value === undefined) return false;
71
+ return (
72
+ value instanceof ArrayBuffer ||
73
+ ArrayBuffer.isView(value) ||
74
+ (typeof Blob !== "undefined" && value instanceof Blob)
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Get the binary type tag for a value
80
+ *
81
+ * Returns a single character tag indicating the binary data type:
82
+ * - `'A'` for ArrayBuffer and TypedArray views
83
+ * - `'B'` for Blob objects
84
+ *
85
+ * This tag is used in the wire protocol to indicate how to
86
+ * reconstruct the binary data on the receiving end.
87
+ *
88
+ * @param {ArrayBuffer|ArrayBufferView|Blob} value - The binary value to tag
89
+ * @returns {'A'|'B'} The type tag character
90
+ *
91
+ * @example
92
+ * getBinaryTag(new ArrayBuffer(10)) // 'A'
93
+ * getBinaryTag(new Uint8Array(10)) // 'A'
94
+ * getBinaryTag(new Blob(['hello'])) // 'B'
95
+ *
96
+ * @example
97
+ * // Used when building upload metadata
98
+ * const tag = getBinaryTag(fileData)
99
+ * const key = `attachment<!${tag}>` // 'attachment<!A>' or 'attachment<!B>'
100
+ */
101
+ export function getBinaryTag(value) {
102
+ if (typeof Blob !== "undefined" && value instanceof Blob) return "B";
103
+ return "A";
104
+ }
105
+
106
+ /**
107
+ * Generate a simple hash for binary upload path identification
108
+ *
109
+ * Creates a short hash string from a path string, used to uniquely
110
+ * identify binary data uploads. Uses a simple string hashing algorithm
111
+ * with base-36 encoding for compact representation.
112
+ *
113
+ * Note: This is not cryptographically secure - it's for identification only.
114
+ *
115
+ * @param {string} path - The property path to hash (e.g., 'user.avatar')
116
+ * @returns {string} Base-36 encoded hash string
117
+ *
118
+ * @example
119
+ * generateUploadHash('image') // e.g., 'k3m9x'
120
+ * generateUploadHash('user.avatar') // e.g., 'p7n2w'
121
+ * generateUploadHash('files.0') // e.g., 'q1r8t'
122
+ *
123
+ * @example
124
+ * // Used in upload processing
125
+ * const hash = generateUploadHash('documents.contract')
126
+ * const uploadUrl = `/api/ape/data/${queryId}/${hash}`
127
+ */
128
+ export function generateUploadHash(path) {
129
+ let hash = 0;
130
+ for (let i = 0; i < path.length; i++) {
131
+ const char = path.charCodeAt(i);
132
+ hash = (hash << 5) - hash + char;
133
+ hash = hash & hash; // Convert to 32-bit integer
134
+ }
135
+ return Math.abs(hash).toString(36);
136
+ }
137
+
138
+ /**
139
+ * Set a value at a nested dot-notation path in an object
140
+ *
141
+ * Traverses an object following a dot-separated path and sets
142
+ * the value at the final key. The path must be valid (all intermediate
143
+ * objects must exist).
144
+ *
145
+ * @param {Object} obj - The object to modify
146
+ * @param {string} path - Dot-notation path (e.g., 'user.profile.avatar')
147
+ * @param {any} value - The value to set at the path
148
+ * @returns {void}
149
+ * @throws {TypeError} If intermediate path segments don't exist
150
+ *
151
+ * @example
152
+ * const data = { user: { profile: { name: 'Alice' } } }
153
+ *
154
+ * setValueAtPath(data, 'user.profile.avatar', new ArrayBuffer(100))
155
+ * // data.user.profile.avatar is now the ArrayBuffer
156
+ *
157
+ * @example
158
+ * // Simple path
159
+ * const obj = { image: 'placeholder' }
160
+ * setValueAtPath(obj, 'image', binaryData)
161
+ *
162
+ * @example
163
+ * // Array index path
164
+ * const obj = { files: [null, null] }
165
+ * setValueAtPath(obj, 'files.0', fileBuffer)
166
+ * setValueAtPath(obj, 'files.1', anotherBuffer)
167
+ */
168
+ export function setValueAtPath(obj, path, value) {
169
+ const parts = path.split(".");
170
+ let current = obj;
171
+
172
+ // Navigate to parent of target property
173
+ for (let i = 0; i < parts.length - 1; i++) {
174
+ current = current[parts[i]];
175
+ }
176
+
177
+ // Set the value at the final key
178
+ current[parts[parts.length - 1]] = value;
179
+ }
180
+
181
+ /**
182
+ * Find properties with a specific tag suffix in a nested object
183
+ *
184
+ * Recursively searches through an object (including arrays) for keys
185
+ * ending with a tag suffix like `<!L>`, `<!F>`, `<!A>`, or `<!B>`.
186
+ * Returns information about each tagged property found.
187
+ *
188
+ * ## Tag Types
189
+ * - `L` - Linked binary resource (server sends hash, client downloads)
190
+ * - `F` - File share (client-to-client transfer)
191
+ * - `A` - ArrayBuffer upload marker
192
+ * - `B` - Blob upload marker
193
+ *
194
+ * @param {Object|Array|any} obj - The object to search
195
+ * @param {string} tag - The tag to look for (without <! and >)
196
+ * @param {string} [path=''] - Current path (used internally for recursion)
197
+ * @returns {Array<{path: string, hash: string, originalKey: string}>} Array of found tagged properties
198
+ *
199
+ * @example
200
+ * // Find server-linked binary references
201
+ * const response = {
202
+ * name: 'Report',
203
+ * 'pdf<!L>': 'hash123',
204
+ * 'thumbnail<!L>': 'hash456'
205
+ * }
206
+ *
207
+ * const links = findTaggedProps(response, 'L')
208
+ * // Returns:
209
+ * // [
210
+ * // { path: 'pdf', hash: 'hash123', originalKey: 'pdf<!L>' },
211
+ * // { path: 'thumbnail', hash: 'hash456', originalKey: 'thumbnail<!L>' }
212
+ * // ]
213
+ *
214
+ * @example
215
+ * // Find nested tagged properties
216
+ * const data = {
217
+ * user: {
218
+ * 'avatar<!L>': 'avatarHash',
219
+ * documents: [
220
+ * { 'file<!L>': 'doc1Hash' },
221
+ * { 'file<!L>': 'doc2Hash' }
222
+ * ]
223
+ * }
224
+ * }
225
+ *
226
+ * const links = findTaggedProps(data, 'L')
227
+ * // Returns paths: 'user.avatar', 'user.documents.0.file', 'user.documents.1.file'
228
+ *
229
+ * @example
230
+ * // Find file share tags
231
+ * const message = { 'attachment<!F>': 'shareHash123' }
232
+ * const shares = findTaggedProps(message, 'F')
233
+ */
234
+ export function findTaggedProps(obj, tag, path = "") {
235
+ const results = [];
236
+
237
+ // Base case: null, undefined, or non-object
238
+ if (obj === null || obj === undefined || typeof obj !== "object") {
239
+ return results;
240
+ }
241
+
242
+ // Handle arrays - recurse into each element
243
+ if (Array.isArray(obj)) {
244
+ for (let i = 0; i < obj.length; i++) {
245
+ results.push(
246
+ ...findTaggedProps(obj[i], tag, path ? `${path}.${i}` : String(i)),
247
+ );
248
+ }
249
+ return results;
250
+ }
251
+
252
+ // Handle objects - check each key for tag suffix
253
+ const suffix = `<!${tag}>`;
254
+
255
+ for (const key of Object.keys(obj)) {
256
+ if (key.endsWith(suffix)) {
257
+ // Found a tagged key - extract the clean name and hash
258
+ const cleanKey = key.slice(0, -4); // Remove <!X> suffix (4 chars)
259
+ results.push({
260
+ path: path ? `${path}.${cleanKey}` : cleanKey,
261
+ hash: obj[key],
262
+ originalKey: key,
263
+ });
264
+ } else {
265
+ // Not tagged - recurse into value
266
+ results.push(
267
+ ...findTaggedProps(obj[key], tag, path ? `${path}.${key}` : key),
268
+ );
269
+ }
270
+ }
271
+
272
+ return results;
273
+ }
274
+
275
+ /**
276
+ * Clean tagged keys from an object (rename `key<!X>` to `key`)
277
+ *
278
+ * Creates a new object with tag suffixes removed from keys.
279
+ * Recursively processes nested objects and arrays.
280
+ *
281
+ * This is used after finding tagged properties to create a clean
282
+ * object structure where the tags have been replaced with actual values.
283
+ *
284
+ * @param {Object|Array|any} obj - The object to clean
285
+ * @param {string} tag - The tag to remove (without <! and >)
286
+ * @returns {Object|Array|any} New object with cleaned keys
287
+ *
288
+ * @example
289
+ * // Clean L-tagged keys
290
+ * const response = {
291
+ * name: 'Photo',
292
+ * 'image<!L>': 'hash123',
293
+ * size: 1024
294
+ * }
295
+ *
296
+ * const cleaned = cleanTaggedKeys(response, 'L')
297
+ * // Returns: { name: 'Photo', image: 'hash123', size: 1024 }
298
+ *
299
+ * @example
300
+ * // Nested cleaning
301
+ * const data = {
302
+ * user: {
303
+ * 'avatar<!L>': 'avatarHash'
304
+ * }
305
+ * }
306
+ *
307
+ * const cleaned = cleanTaggedKeys(data, 'L')
308
+ * // Returns: { user: { avatar: 'avatarHash' } }
309
+ *
310
+ * @example
311
+ * // Array handling
312
+ * const files = [
313
+ * { 'data<!L>': 'hash1' },
314
+ * { 'data<!L>': 'hash2' }
315
+ * ]
316
+ *
317
+ * const cleaned = cleanTaggedKeys(files, 'L')
318
+ * // Returns: [{ data: 'hash1' }, { data: 'hash2' }]
319
+ */
320
+ export function cleanTaggedKeys(obj, tag) {
321
+ // Base case: null, undefined, or non-object - return as-is
322
+ if (obj === null || obj === undefined || typeof obj !== "object") {
323
+ return obj;
324
+ }
325
+
326
+ // Handle arrays - map each element
327
+ if (Array.isArray(obj)) {
328
+ return obj.map((item) => cleanTaggedKeys(item, tag));
329
+ }
330
+
331
+ // Handle objects - process each key
332
+ const cleaned = {};
333
+ const suffix = `<!${tag}>`;
334
+
335
+ for (const key of Object.keys(obj)) {
336
+ if (key.endsWith(suffix)) {
337
+ // Remove the tag suffix from the key
338
+ cleaned[key.slice(0, -4)] = obj[key];
339
+ } else {
340
+ // Keep key as-is, but recursively clean the value
341
+ cleaned[key] = cleanTaggedKeys(obj[key], tag);
342
+ }
343
+ }
344
+
345
+ return cleaned;
346
+ }
@@ -0,0 +1,71 @@
1
+ # Connection Module Files
2
+
3
+ This module handles the core mechanics of maintaining a WebSocket connection to an api-ape server. It manages connection state, message sending, request/response correlation, and binary file transfers.
4
+
5
+ ## Guidelines
6
+
7
+ - **Browser-safe only** — All code must run in browsers without Node.js APIs
8
+ - **State consistency** — Connection state changes must flow through `state.js`
9
+ - **QueryId correlation** — All requests must use `sender.js` for proper response matching
10
+ - **Binary handling** — File transfers use the tag system (`<!B>`, `<!A>`, `<!L>`) — coordinate with `fileHandling.js`
11
+ - **Network detection** — Changes to connectivity detection should account for captive portals
12
+
13
+ ## Directory Structure
14
+
15
+ ```
16
+ connection/
17
+ ├── fileDownload.js # Binary file download handling
18
+ ├── fileHandling.js # File upload/download coordination
19
+ ├── fileUtils.js # File transfer utilities
20
+ ├── messageHandler.js # Message processing and dispatch
21
+ ├── network.js # Network state detection
22
+ ├── proxy.js # Proxy-based API generation
23
+ ├── sender.js # Message sending with queryId correlation
24
+ ├── state.js # Connection state machine
25
+ └── subscriptions.js # Pub/sub subscription manager
26
+ ```
27
+
28
+ ## Files
29
+
30
+ ### `proxy.js`
31
+
32
+ Creates the Proxy-based API that allows `api.users.list()` syntax. Intercepts property access and method calls, converting them into WebSocket messages with the appropriate endpoint path.
33
+
34
+ ### `sender.js`
35
+
36
+ Handles message serialization and request/response correlation. Generates unique `queryId` for each request, tracks pending requests, manages timeouts, and resolves Promises when responses arrive.
37
+
38
+ ### `state.js`
39
+
40
+ Connection state machine that tracks the current connection status. Emits state change events and provides the `onConnectionChange` callback functionality.
41
+
42
+ **States:** `offline` → `connecting` → `connected` → `disconnected` → `walled`
43
+
44
+ ### `network.js`
45
+
46
+ Detects network conditions to provide accurate connection state:
47
+ - **Offline** — Browser reports no network (`navigator.onLine`)
48
+ - **Walled garden** — Captive portal detected (WiFi login page)
49
+ - **Online** — Full internet connectivity confirmed
50
+
51
+ ### `fileDownload.js`
52
+
53
+ Handles downloading binary data from the server. When a response contains `<!L>` (link) tags, fetches the binary data via HTTP GET with session cookies for authentication.
54
+
55
+ ### `fileHandling.js`
56
+
57
+ Coordinates file uploads and downloads by detecting special tags in messages:
58
+ - `<!B>` / `<!A>` — Binary data that needs to be uploaded
59
+ - `<!L>` — Download links that need to be fetched
60
+
61
+ ### `fileUtils.js`
62
+
63
+ Shared utilities for file transfer operations including content type detection, hash generation, and ArrayBuffer/Buffer conversions.
64
+
65
+ ### `messageHandler.js`
66
+
67
+ Handles incoming message processing and dispatch. Processes binary data hydration (fetching linked resources) and dispatches messages to registered handlers and subscription callbacks. Provides the `setOnReceiver` function for registering message handlers.
68
+
69
+ ### `subscriptions.js`
70
+
71
+ Manages channel subscriptions for the chained subscription syntax. Tracks local callbacks per channel, sends subscribe/unsubscribe messages to server, dispatches incoming data to callbacks, and handles resubscription on reconnect.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @fileoverview Message handling utilities for api-ape client
3
+ *
4
+ * Handles incoming message processing, including binary data hydration
5
+ * and dispatching to registered handlers and subscription callbacks.
6
+ *
7
+ * @module client/connection/messageHandler
8
+ */
9
+
10
+ import {
11
+ fetchLinkedResources,
12
+ fetchSharedFiles,
13
+ } from "./fileDownload";
14
+ import {
15
+ hasSubscribers,
16
+ dispatch as dispatchToSubscribers,
17
+ } from "./subscriptions";
18
+
19
+ /**
20
+ * Array of universal message receivers (called for all message types)
21
+ * @type {Array<function({err: any, type: string, data: any}): void>}
22
+ * @private
23
+ */
24
+ const receiverArray = [];
25
+
26
+ /**
27
+ * Map of type-specific message receivers
28
+ * @type {Object.<string, Array<function({err: any, type: string, data: any}): void>>}
29
+ * @private
30
+ */
31
+ const ofTypesOb = {};
32
+
33
+ /**
34
+ * Process incoming message data to hydrate binary resources
35
+ *
36
+ * This function handles two types of binary data references:
37
+ * - L-tagged: Binary data linked from server responses (fetchLinkedResources)
38
+ * - F-tagged: Shared files from other clients (fetchSharedFiles)
39
+ *
40
+ * @param {any} data - Raw data from server message
41
+ * @param {Error|null} err - Error from the message, if any
42
+ * @returns {Promise<any>} Hydrated data with binary resources fetched
43
+ *
44
+ * @example
45
+ * // Server sends: { image<!L>: 'abc123' }
46
+ * // After hydration: { image: ArrayBuffer }
47
+ */
48
+ export async function processIncomingData(data, err) {
49
+ if (!data || err) return data;
50
+ try {
51
+ let result = await fetchLinkedResources(data);
52
+ return await fetchSharedFiles(result);
53
+ } catch (e) {
54
+ console.error(`🦍 Failed to hydrate data:`, e);
55
+ return data;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Dispatch a received message to all registered handlers
61
+ *
62
+ * Messages are delivered to:
63
+ * 1. Chained subscription callbacks (new v2 syntax)
64
+ * 2. Type-specific handlers registered via setOnReceiver(type, handler)
65
+ * 3. Universal handlers registered via setOnReceiver(handler)
66
+ *
67
+ * @param {string} type - Message type identifier
68
+ * @param {Error|null} err - Error payload, if any
69
+ * @param {any} data - Message data payload
70
+ */
71
+ export function dispatchMessage(type, err, data) {
72
+ // Dispatch to chained subscription callbacks (v2 syntax)
73
+ if (hasSubscribers(type)) {
74
+ dispatchToSubscribers(type, data);
75
+ }
76
+
77
+ // Legacy handlers
78
+ if (ofTypesOb[type]) ofTypesOb[type].forEach((w) => w({ err, type, data }));
79
+ receiverArray.forEach((w) => w({ err, type, data }));
80
+ }
81
+
82
+ /**
83
+ * Register a message receiver/handler
84
+ *
85
+ * @param {string|function} onTypeStFn - Message type to listen for, or universal handler function
86
+ * @param {function=} handlerFn - Handler function (if first arg is type string)
87
+ *
88
+ * @example
89
+ * // Type-specific handler
90
+ * setOnReceiver('notification', (msg) => {
91
+ * console.log('Got notification:', msg.data)
92
+ * })
93
+ *
94
+ * // Universal handler (receives all messages)
95
+ * setOnReceiver((msg) => {
96
+ * console.log('Got message:', msg.type, msg.data)
97
+ * })
98
+ */
99
+ export function setOnReceiver(onTypeStFn, handlerFn) {
100
+ if (typeof onTypeStFn === "string") {
101
+ ofTypesOb[onTypeStFn] = [handlerFn];
102
+ } else if (!receiverArray.includes(onTypeStFn)) {
103
+ receiverArray.push(onTypeStFn);
104
+ }
105
+ }