api-ape 3.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +59 -572
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -203
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +31 -6
  44. package/server/README.md +272 -67
  45. package/server/adapters/README.md +23 -14
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +322 -71
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -219
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -224
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -311
  186. package/server/lib/broadcast.js +0 -146
@@ -0,0 +1,511 @@
1
+ /**
2
+ * @fileoverview Node.js / Express Runtime Integration for api-ape
3
+ *
4
+ * This module provides runtime-specific integration for running api-ape
5
+ * on Node.js HTTP servers. It handles WebSocket upgrades, HTTP routes for
6
+ * file transfers, and integrates with existing Express/Node.js request handlers.
7
+ *
8
+ * ## Node.js Server Architecture
9
+ *
10
+ * Node.js HTTP servers use a different pattern than Bun:
11
+ * - HTTP requests handled via 'request' event listeners
12
+ * - WebSocket upgrades handled via 'upgrade' event
13
+ * - Multiple request listeners can coexist
14
+ *
15
+ * ## Integration Approach
16
+ *
17
+ * This module injects api-ape handlers into an existing HTTP server by:
18
+ * 1. Adding a WebSocket upgrade handler for the api-ape endpoint
19
+ * 2. Prepending a request handler that intercepts api-ape routes
20
+ * 3. Delegating non-api-ape requests to existing handlers
21
+ *
22
+ * ## Routes Handled
23
+ *
24
+ * The request handler manages these api-ape routes:
25
+ * - `/{where}/ape.js` - Client JavaScript bundle
26
+ * - `/{where}/ape.js.map` - Source map for debugging
27
+ * - `/{where}/ping` - Health check endpoint
28
+ * - `/{where}/poll` (GET) - Long polling stream endpoint
29
+ * - `/{where}/poll` (POST) - Long polling message endpoint
30
+ * - `/{where}/download/:hash` - File download endpoint
31
+ * - `/{where}/upload/:queryId/:hash` - File upload endpoint
32
+ *
33
+ * @module server/lib/runtimes/node
34
+ * @see {@link module:server/lib/runtimes/bun} - Bun runtime equivalent
35
+ * @see {@link module:server/lib/wsProvider} - WebSocket provider selection
36
+ * @see {@link module:server/lib/httpUtils} - HTTP utility functions
37
+ *
38
+ * @example
39
+ * // Basic integration with Node.js HTTP server
40
+ * const http = require('http')
41
+ * const { initNodeServer } = require('./runtimes/node')
42
+ *
43
+ * const server = http.createServer((req, res) => {
44
+ * res.writeHead(200)
45
+ * res.end('Hello World')
46
+ * })
47
+ *
48
+ * const core = prepareCore({ where: 'api', onConnect })
49
+ * initNodeServer(server, { where: 'api' }, core)
50
+ *
51
+ * server.listen(3000)
52
+ *
53
+ * @example
54
+ * // Integration with Express
55
+ * const express = require('express')
56
+ * const { initNodeServer } = require('./runtimes/node')
57
+ *
58
+ * const app = express()
59
+ * app.get('/', (req, res) => res.send('Hello'))
60
+ *
61
+ * const server = app.listen(3000)
62
+ *
63
+ * const core = prepareCore({ where: 'api', onConnect })
64
+ * initNodeServer(server, { where: 'api' }, core)
65
+ *
66
+ * @example
67
+ * // With authentication middleware
68
+ * const { initNodeServer } = require('./runtimes/node')
69
+ *
70
+ * const core = prepareCore({
71
+ * where: 'api',
72
+ * onConnect: async (socket, req, send) => {
73
+ * const token = req.headers.cookie?.match(/token=([^;]+)/)?.[1]
74
+ * const user = await verifyToken(token)
75
+ *
76
+ * if (!user) {
77
+ * socket.close(4001, 'Unauthorized')
78
+ * return null
79
+ * }
80
+ *
81
+ * return {
82
+ * embed: { userId: user.id },
83
+ * onDisconnect: () => console.log('User disconnected')
84
+ * }
85
+ * }
86
+ * })
87
+ *
88
+ * initNodeServer(server, options, core)
89
+ */
90
+
91
+ const { getWebSocketProvider } = require("../wsProvider");
92
+ const { parse: parseUrl } = require("url");
93
+ const {
94
+ matchRoute,
95
+ sendJson,
96
+ getCookie,
97
+ isLocalhost,
98
+ isSecure,
99
+ serveClientBundle,
100
+ serveSourceMap,
101
+ } = require("../httpUtils");
102
+
103
+ /**
104
+ * @typedef {Object} NodeServerResult
105
+ * Result from initializing the Node.js server integration.
106
+ *
107
+ * @property {WebSocketServer} wss - The WebSocket server instance
108
+ * @property {Object} core - Reference to the core api-ape configuration
109
+ */
110
+
111
+ /**
112
+ * Initialize api-ape on an existing Node.js HTTP server.
113
+ *
114
+ * This function integrates api-ape into a Node.js/Express server by:
115
+ * 1. Creating a WebSocket server in noServer mode
116
+ * 2. Adding an upgrade handler for WebSocket connections
117
+ * 3. Injecting a request handler for api-ape HTTP routes
118
+ * 4. Preserving existing request handlers
119
+ *
120
+ * ## WebSocket Upgrade
121
+ *
122
+ * Listens for the 'upgrade' event on the HTTP server and handles
123
+ * WebSocket upgrades for the api-ape endpoint path. Non-api-ape
124
+ * upgrade requests are destroyed.
125
+ *
126
+ * ## Request Handling
127
+ *
128
+ * The function captures existing 'request' listeners and replaces them
129
+ * with a single handler that:
130
+ * 1. Checks if the request matches an api-ape route
131
+ * 2. Handles api-ape routes (client bundle, polling, file transfer)
132
+ * 3. Delegates non-api-ape requests to the original handlers
133
+ *
134
+ * ## Routes Handled
135
+ *
136
+ * | Path | Method | Description |
137
+ * |------|--------|-------------|
138
+ * | `/{where}/ape.js` | GET | Client JavaScript bundle |
139
+ * | `/{where}/ape.js.map` | GET | Source map for debugging |
140
+ * | `/{where}/ping` | GET | Health check (returns `{ ok: true, ts }`) |
141
+ * | `/{where}/poll` | GET | Long polling stream endpoint |
142
+ * | `/{where}/poll` | POST | Long polling message endpoint |
143
+ * | `/{where}/download/:hash` | GET | File download |
144
+ * | `/{where}/upload/:qid/:hash` | PUT | File upload |
145
+ *
146
+ * @function initNodeServer
147
+ * @param {http.Server} server - Node.js HTTP server instance
148
+ * @param {Object} options - Server configuration options
149
+ * @param {string} options.where - Base path for api-ape endpoints
150
+ * @param {Object} core - Core api-ape configuration from prepareCore()
151
+ * @param {string} core.wsPath - WebSocket endpoint path
152
+ * @param {string} core.clientPath - Client bundle path
153
+ * @param {string} core.clientMapPath - Source map path
154
+ * @param {string} core.pingPath - Health check path
155
+ * @param {string} core.pollPath - Long polling path
156
+ * @param {string} core.downloadPattern - Download route pattern
157
+ * @param {string} core.uploadPattern - Upload route pattern
158
+ * @param {Function} core.wiringHandler - Handler for WebSocket connections
159
+ * @param {Function} core.handleStreamGet - Long polling GET handler
160
+ * @param {Function} core.handleStreamPost - Long polling POST handler
161
+ * @param {Object} core.controllers - Loaded controller functions
162
+ * @param {Object} core.fileTransfer - File transfer manager
163
+ * @returns {NodeServerResult} Object with wss and core properties
164
+ *
165
+ * @example
166
+ * // Basic Node.js HTTP server
167
+ * const http = require('http')
168
+ *
169
+ * const server = http.createServer((req, res) => {
170
+ * res.writeHead(404)
171
+ * res.end('Not Found')
172
+ * })
173
+ *
174
+ * const { initNodeServer } = require('./runtimes/node')
175
+ * const { wss, core } = initNodeServer(server, options, coreConfig)
176
+ *
177
+ * console.log(`WebSocket server ready with ${wss.clients.size} clients`)
178
+ *
179
+ * @example
180
+ * // Express integration
181
+ * const express = require('express')
182
+ * const app = express()
183
+ *
184
+ * app.get('/', (req, res) => res.send('Home'))
185
+ * app.get('/api/status', (req, res) => res.json({ status: 'ok' }))
186
+ *
187
+ * const server = app.listen(3000)
188
+ * initNodeServer(server, { where: 'api' }, core)
189
+ *
190
+ * // Both Express routes and api-ape routes work:
191
+ * // GET / -> Express handler
192
+ * // GET /api/status -> Express handler
193
+ * // GET /api/ape.js -> api-ape client bundle
194
+ * // WS /api/ape -> api-ape WebSocket
195
+ *
196
+ * @example
197
+ * // Access WebSocket server for custom logic
198
+ * const { wss } = initNodeServer(server, options, core)
199
+ *
200
+ * // Broadcast to all WebSocket clients
201
+ * setInterval(() => {
202
+ * for (const client of wss.clients) {
203
+ * if (client.readyState === 1) {
204
+ * client.send(JSON.stringify({ type: 'tick', time: Date.now() }))
205
+ * }
206
+ * }
207
+ * }, 1000)
208
+ */
209
+ function initNodeServer(server, options, core) {
210
+ // Get the appropriate WebSocket provider (ws library or polyfill)
211
+ const { WebSocketServer } = getWebSocketProvider();
212
+
213
+ /**
214
+ * WebSocket server instance in noServer mode.
215
+ * This allows manual handling of upgrade requests.
216
+ * @type {WebSocketServer}
217
+ */
218
+ const wss = new WebSocketServer({ noServer: true });
219
+
220
+ // Connect WebSocket server to api-ape wiring handler
221
+ wss.on("connection", core.wiringHandler);
222
+
223
+ /**
224
+ * Handle HTTP upgrade requests for WebSocket connections.
225
+ *
226
+ * Only upgrades requests to the api-ape WebSocket path.
227
+ * All other upgrade requests are rejected by destroying the socket.
228
+ */
229
+ server.on("upgrade", (req, socket, head) => {
230
+ const { pathname } = parseUrl(req.url);
231
+
232
+ if (pathname === core.wsPath) {
233
+ // Handle api-ape WebSocket upgrade
234
+ wss.handleUpgrade(req, socket, head, (ws) => {
235
+ wss.emit("connection", ws, req);
236
+ });
237
+ } else {
238
+ // Reject non-api-ape WebSocket upgrades
239
+ socket.destroy();
240
+ }
241
+ });
242
+
243
+ /**
244
+ * Capture existing request listeners to preserve them.
245
+ * We'll call them for non-api-ape routes.
246
+ * @type {Function[]}
247
+ */
248
+ const originalListeners = server.listeners("request").slice();
249
+
250
+ // Remove all request listeners - we'll add a single handler
251
+ // that delegates to the originals when appropriate
252
+ server.removeAllListeners("request");
253
+
254
+ /**
255
+ * Unified request handler for api-ape and original routes.
256
+ *
257
+ * Checks each request against api-ape routes first, then
258
+ * delegates to original handlers if not matched.
259
+ */
260
+ server.on("request", (req, res) => {
261
+ const { pathname } = parseUrl(req.url);
262
+
263
+ // Serve client JavaScript bundle
264
+ if (pathname === core.clientPath) {
265
+ return serveClientBundle(core.clientPath, res);
266
+ }
267
+
268
+ // Serve source map for debugging
269
+ if (pathname === core.clientMapPath) {
270
+ return serveSourceMap(res);
271
+ }
272
+
273
+ // Health check endpoint
274
+ if (pathname === core.pingPath && req.method === "GET") {
275
+ return sendJson(res, 200, { ok: true, ts: Date.now() });
276
+ }
277
+
278
+ // Long polling GET - streaming response
279
+ if (pathname === core.pollPath && req.method === "GET") {
280
+ core.handleStreamGet(req, res);
281
+ return;
282
+ }
283
+
284
+ // Long polling POST - client messages
285
+ if (pathname === core.pollPath && req.method === "POST") {
286
+ core.handleStreamPost(req, res, core.controllers);
287
+ return;
288
+ }
289
+
290
+ // File download endpoint
291
+ const downloadMatch = matchRoute(pathname, core.downloadPattern);
292
+ if (req.method === "GET" && downloadMatch) {
293
+ return handleDownload(req, res, downloadMatch.hash, core);
294
+ }
295
+
296
+ // File upload endpoint
297
+ const uploadMatch = matchRoute(pathname, core.uploadPattern);
298
+ if (req.method === "PUT" && uploadMatch) {
299
+ return handleUpload(req, res, uploadMatch, core);
300
+ }
301
+
302
+ // Not an api-ape route - delegate to original handlers
303
+ for (const listener of originalListeners) {
304
+ listener.call(server, req, res);
305
+ }
306
+ });
307
+
308
+ return { wss, core };
309
+ }
310
+
311
+ /**
312
+ * Handle file download requests.
313
+ *
314
+ * Supports two types of downloads:
315
+ * 1. **Streaming files**: Files being transferred between clients
316
+ * 2. **Standard downloads**: Files registered for download by a client
317
+ *
318
+ * ## Streaming Files
319
+ *
320
+ * When a file is being streamed between clients, the server acts as a
321
+ * relay. The download response includes headers indicating completion
322
+ * status and total bytes received so far.
323
+ *
324
+ * ## Security
325
+ *
326
+ * - HTTPS required for non-localhost requests
327
+ * - Client ID from cookie or header required for authorization
328
+ * - Download hash must be registered and authorized for the client
329
+ *
330
+ * @function handleDownload
331
+ * @param {http.IncomingMessage} req - The HTTP request
332
+ * @param {http.ServerResponse} res - The HTTP response
333
+ * @param {string} hash - The download hash from the URL
334
+ * @param {Object} core - Core api-ape configuration
335
+ * @returns {void}
336
+ * @private
337
+ *
338
+ * @example
339
+ * // Download URL format: /{where}/download/{hash}
340
+ * // GET /api/download/abc123xyz
341
+ *
342
+ * @example
343
+ * // Streaming file response headers:
344
+ * // Content-Type: application/octet-stream
345
+ * // X-Ape-Complete: 0 (or 1 when complete)
346
+ * // X-Ape-Total-Received: 1234 (bytes received so far)
347
+ */
348
+ function handleDownload(req, res, hash, core) {
349
+ // Check for streaming file first (client-to-client transfer)
350
+ const streamingFile = core.fileTransfer.getStreamingFile(hash);
351
+
352
+ /* istanbul ignore if - streaming file download, requires client-to-client streaming setup */
353
+ if (streamingFile) {
354
+ // Security: Require HTTPS for non-localhost
355
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
356
+ return sendJson(res, 403, {
357
+ error: "HTTPS required for file transfers",
358
+ });
359
+ }
360
+
361
+ // Send streaming file with progress headers
362
+ res.writeHead(200, {
363
+ "Content-Type": "application/octet-stream",
364
+ "Content-Length": streamingFile.data.length,
365
+ "X-Ape-Complete": streamingFile.isComplete ? "1" : "0",
366
+ "X-Ape-Total-Received": String(streamingFile.totalReceived),
367
+ });
368
+ res.end(streamingFile.data);
369
+ return;
370
+ }
371
+
372
+ // Standard download - requires client authentication
373
+ const clientId =
374
+ getCookie(req.headers, "apeClientId") || req.headers["x-ape-client-id"];
375
+
376
+ if (!clientId) {
377
+ return sendJson(res, 401, { error: "Missing session identifier" });
378
+ }
379
+
380
+ // Security: Require HTTPS for non-localhost
381
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
382
+ return sendJson(res, 403, { error: "HTTPS required for file transfers" });
383
+ }
384
+
385
+ // Get the download data for this client
386
+ const result = core.fileTransfer.getDownload(hash, clientId);
387
+
388
+ if (!result) {
389
+ return sendJson(res, 404, { error: "Download not found or unauthorized" });
390
+ }
391
+
392
+ // Send the file data
393
+ res.writeHead(200, {
394
+ "Content-Type": result.contentType,
395
+ "Content-Length": result.data.length || result.data.byteLength,
396
+ });
397
+ res.end(result.data);
398
+ }
399
+
400
+ /**
401
+ * Handle file upload requests.
402
+ *
403
+ * Supports two types of uploads:
404
+ * 1. **Streaming uploads**: Completing a streaming file transfer
405
+ * 2. **Standard uploads**: Binary data for pending message fields
406
+ *
407
+ * ## Upload Flow
408
+ *
409
+ * 1. Client sends a message with `<!B>` or `<!A>` tagged fields
410
+ * 2. Server holds the message pending binary data
411
+ * 3. Client uploads binary data via PUT to this endpoint
412
+ * 4. Server completes the message with the uploaded data
413
+ * 5. Controller is invoked with the complete message
414
+ *
415
+ * ## Security
416
+ *
417
+ * - HTTPS required for non-localhost requests
418
+ * - Client ID required for authorization
419
+ * - Query ID and path hash must match pending upload
420
+ *
421
+ * @function handleUpload
422
+ * @param {http.IncomingMessage} req - The HTTP request
423
+ * @param {http.ServerResponse} res - The HTTP response
424
+ * @param {Object} match - Route match containing queryId and pathHash
425
+ * @param {string} match.queryId - Query ID from pending message
426
+ * @param {string} match.pathHash - Hash identifying the upload field
427
+ * @param {Object} core - Core api-ape configuration
428
+ * @returns {void}
429
+ * @private
430
+ *
431
+ * @example
432
+ * // Upload URL format: /{where}/upload/{queryId}/{pathHash}
433
+ * // PUT /api/upload/K7M3NP2Q/abc123xyz
434
+ * // Body: <binary data>
435
+ *
436
+ * @example
437
+ * // Success response:
438
+ * // { "success": true }
439
+ *
440
+ * // Streaming success response:
441
+ * // { "success": true, "streaming": true }
442
+ *
443
+ * @example
444
+ * // Error responses:
445
+ * // 401: { "error": "Missing session identifier" }
446
+ * // 403: { "error": "HTTPS required for file transfers" }
447
+ * // 404: { "error": "Upload not expected or unauthorized" }
448
+ */
449
+ function handleUpload(req, res, match, core) {
450
+ const { queryId, pathHash } = match;
451
+
452
+ // Security: Require HTTPS for non-localhost
453
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
454
+ return sendJson(res, 403, { error: "HTTPS required for file transfers" });
455
+ }
456
+
457
+ // Collect request body chunks
458
+ const chunks = [];
459
+ req.on("data", (chunk) => chunks.push(chunk));
460
+
461
+ req.on("end", () => {
462
+ // Concatenate all chunks into a single Buffer
463
+ const data = Buffer.concat(chunks);
464
+
465
+ /* istanbul ignore if - streaming file upload, requires client-to-client streaming setup */
466
+ // Check if this is a streaming file upload
467
+ if (core.fileTransfer.isStreamingFile(pathHash)) {
468
+ const success = core.fileTransfer.completeStreamingUpload(pathHash, data);
469
+
470
+ if (success) {
471
+ return sendJson(res, 200, { success: true, streaming: true });
472
+ }
473
+
474
+ return sendJson(res, 404, { error: "Streaming file not found" });
475
+ }
476
+
477
+ // Standard upload - requires client authentication
478
+ const clientId =
479
+ getCookie(req.headers, "apeClientId") || req.headers["x-ape-client-id"];
480
+
481
+ if (!clientId) {
482
+ return sendJson(res, 401, { error: "Missing session identifier" });
483
+ }
484
+
485
+ // Attempt to receive the upload
486
+ const success = core.fileTransfer.receiveUpload(
487
+ queryId,
488
+ pathHash,
489
+ data,
490
+ clientId,
491
+ );
492
+
493
+ if (success) {
494
+ sendJson(res, 200, { success: true });
495
+ } else {
496
+ sendJson(res, 404, { error: "Upload not expected or unauthorized" });
497
+ }
498
+ });
499
+
500
+ // Handle request errors
501
+ /* istanbul ignore next - request error handler, requires network failure */
502
+ req.on("error", (err) => sendJson(res, 500, { error: err.message }));
503
+ }
504
+
505
+ module.exports = {
506
+ /**
507
+ * Initialize api-ape on an existing Node.js HTTP server.
508
+ * @function
509
+ */
510
+ initNodeServer,
511
+ };