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
|
@@ -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
|
+
};
|