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,111 @@
1
+ # Server Lib Module Files
2
+
3
+ This is the core implementation of api-ape's server functionality. It orchestrates everything needed to transform a standard HTTP server into a real-time WebSocket API server with automatic controller routing, client management, and message handling.
4
+
5
+ ## Guidelines
6
+
7
+ - **Runtime abstraction** — All code must work across Node.js, Bun, and Deno; use `wsProvider.js` for detection
8
+ - **No external dependencies** — Use the built-in WebSocket polyfill (`ws/`) instead of external packages
9
+ - **Controller loading** — New controller loading logic should go in `loader.js`; maintain the file-path-to-endpoint mapping convention
10
+ - **Client tracking** — Use `broadcast.js` for client registration; don't maintain separate client lists
11
+ - **Binary transfers** — Coordinate with `fileTransfer.js` for all binary data; use the tag system consistently
12
+ - **HTTP fallback** — Changes to WebSocket handling should have corresponding long-polling support
13
+
14
+ ## Directory Structure
15
+
16
+ ```
17
+ lib/
18
+ ├── main.js # Server initialization entry point
19
+ ├── loader.js # Controller file auto-loader
20
+ ├── broadcast.js # Client tracking & broadcast utilities
21
+ ├── bun.js # Bun.serve() high-level integration
22
+ ├── fileTransfer.js # Binary file transfer manager
23
+ ├── fileTransfer.test.js # File transfer test suite
24
+ ├── httpUtils.js # Shared HTTP utilities
25
+ ├── longPolling.js # HTTP streaming fallback coordinator
26
+ ├── wiring.js # WebSocket connection lifecycle handler
27
+ ├── wsProvider.js # Runtime detection & WebSocket provider selection
28
+ ├── fileTransfer/ # File transfer sub-modules
29
+ ├── longPolling/ # Long-polling sub-modules
30
+ ├── runtimes/ # Runtime-specific server initialization
31
+ └── ws/ # RFC 6455 WebSocket polyfill
32
+ ```
33
+
34
+ ## Files
35
+
36
+ ### `main.js`
37
+
38
+ Main entry point for initializing api-ape on an HTTP server. Detects the runtime environment, creates core handlers, and delegates to the appropriate runtime-specific initializer.
39
+
40
+ ### `loader.js`
41
+
42
+ Recursively loads controller files from a directory, mapping file paths to endpoint names:
43
+ - `api/users.js` → `controllers['users']`
44
+ - `api/users/profile.js` → `controllers['users/profile']`
45
+ - `api/users/index.js` → `controllers['users']`
46
+
47
+ ### `broadcast.js`
48
+
49
+ Manages connected clients and provides messaging utilities:
50
+ - `broadcast(type, data)` — Send to all clients
51
+ - `broadcastOthers(type, data, excludeClientId)` — Send to all except one
52
+ - `publish(channel, data)` — Send to all subscribers of a channel
53
+ - `subscribe(clientId, channel)` — Subscribe a client to a channel
54
+ - `unsubscribe(clientId, channel)` — Unsubscribe a client from a channel
55
+ - `clients` — Read-only Map of connected clients with `send()` method
56
+
57
+ ### `bun.js`
58
+
59
+ High-level Bun integration that returns `fetch` and `websocket` handlers ready for `Bun.serve()`. Handles WebSocket upgrades, client bundle serving, and all api-ape routes.
60
+
61
+ ### `fileTransfer.js`
62
+
63
+ Manages binary file uploads and downloads:
64
+ - Registers pending uploads with timeout handling
65
+ - Validates upload authorization via client session
66
+ - Coordinates streaming file transfers between clients
67
+
68
+ ### `httpUtils.js`
69
+
70
+ Shared HTTP utilities used across the server:
71
+ - `matchRoute(path, pattern)` — URL pattern matching with parameter extraction
72
+ - `sendJson(res, status, data)` — JSON response helper
73
+ - `getCookie(headers, name)` — Cookie extraction
74
+ - `isSecure(req)` / `isLocalhost(host)` — Security checks
75
+
76
+ ### `longPolling.js`
77
+
78
+ Coordinates HTTP long-polling as a WebSocket fallback. Creates and manages the GET (streaming) and POST (messaging) handlers that share client state.
79
+
80
+ ### `wiring.js`
81
+
82
+ Sets up WebSocket connection lifecycle:
83
+ - Generates unique client IDs
84
+ - Parses User-Agent for client info
85
+ - Registers clients in broadcast system
86
+ - Invokes `onConnect` callback with lifecycle hooks
87
+ - Routes messages to controllers via `socket/receive.js`
88
+
89
+ ### `wsProvider.js`
90
+
91
+ Detects runtime environment and returns the appropriate WebSocket implementation:
92
+ 1. Deno → Native `Deno.upgradeWebSocket()`
93
+ 2. Bun → Native Bun WebSocket
94
+ 3. Node.js 24+ → Native `node:ws` module
95
+ 4. Fallback → Built-in RFC 6455 polyfill
96
+
97
+ ### `fileTransfer/`
98
+
99
+ File transfer sub-modules for streaming transfers. See [`fileTransfer/files.md`](./fileTransfer/files.md).
100
+
101
+ ### `longPolling/`
102
+
103
+ HTTP long-polling handlers for GET (streaming) and POST (messaging). See [`longPolling/files.md`](./longPolling/files.md).
104
+
105
+ ### `runtimes/`
106
+
107
+ Runtime-specific server initialization for Node.js and Bun. See [`runtimes/files.md`](./runtimes/files.md).
108
+
109
+ ### `ws/`
110
+
111
+ RFC 6455 WebSocket polyfill and runtime adapters. See [`ws/files.md`](./ws/files.md).
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @fileoverview Shared HTTP Utilities for api-ape Server
3
+ *
4
+ * This module provides common HTTP utility functions used across the api-ape
5
+ * server implementation. These utilities handle:
6
+ *
7
+ * - URL route matching with parameter extraction
8
+ * - JSON response formatting
9
+ * - Cookie parsing
10
+ * - Security checks (localhost detection, HTTPS validation)
11
+ * - Static file serving (client bundle and source maps)
12
+ *
13
+ * @module server/lib/httpUtils
14
+ * @see {@link module:server/lib/main} - Main server module using these utilities
15
+ * @see {@link module:server/lib/runtimes/node} - Node.js runtime using these utilities
16
+ *
17
+ * @example
18
+ * const { matchRoute, sendJson, getCookie, isSecure } = require('./httpUtils')
19
+ *
20
+ * // Match a route with parameters
21
+ * const params = matchRoute('/api/ape/data/abc123', '/api/ape/data/:hash')
22
+ * console.log(params) // { hash: 'abc123' }
23
+ *
24
+ * // Send JSON response
25
+ * sendJson(res, 200, { success: true })
26
+ *
27
+ * // Check security
28
+ * if (!isSecure(req) && !isLocalhost(req.headers.host)) {
29
+ * sendJson(res, 403, { error: 'HTTPS required' })
30
+ * }
31
+ */
32
+
33
+ const path = require("path");
34
+ const fs = require("fs");
35
+
36
+ /**
37
+ * Parses URL path parameters by matching against a pattern.
38
+ *
39
+ * Supports route patterns with colon-prefixed parameters like Express.js.
40
+ * Returns null if the path doesn't match the pattern.
41
+ *
42
+ * @function matchRoute
43
+ * @param {string} pathname - The actual URL pathname to match
44
+ * @param {string} pattern - The route pattern with parameters (e.g., '/api/:id')
45
+ * @returns {Object|null} Object with extracted parameters, or null if no match
46
+ *
47
+ * @example
48
+ * // Basic parameter extraction
49
+ * matchRoute('/api/users/123', '/api/users/:id')
50
+ * // Returns: { id: '123' }
51
+ *
52
+ * @example
53
+ * // Multiple parameters
54
+ * matchRoute('/api/users/123/posts/456', '/api/users/:userId/posts/:postId')
55
+ * // Returns: { userId: '123', postId: '456' }
56
+ *
57
+ * @example
58
+ * // No match (different segment count)
59
+ * matchRoute('/api/users', '/api/users/:id')
60
+ * // Returns: null
61
+ *
62
+ * @example
63
+ * // No match (different static segment)
64
+ * matchRoute('/api/posts/123', '/api/users/:id')
65
+ * // Returns: null
66
+ */
67
+ function matchRoute(pathname, pattern) {
68
+ const patternParts = pattern.split("/");
69
+ const pathParts = pathname.split("/");
70
+
71
+ // Must have same number of segments
72
+ if (patternParts.length !== pathParts.length) return null;
73
+
74
+ const params = {};
75
+
76
+ for (let i = 0; i < patternParts.length; i++) {
77
+ if (patternParts[i].startsWith(":")) {
78
+ // Parameter segment - extract value
79
+ params[patternParts[i].slice(1)] = pathParts[i];
80
+ } else if (patternParts[i] !== pathParts[i]) {
81
+ /* istanbul ignore next - static segment mismatch path */
82
+ // Static segment doesn't match
83
+ return null;
84
+ }
85
+ }
86
+
87
+ return params;
88
+ }
89
+
90
+ /**
91
+ * Sends a JSON response with the specified status code.
92
+ *
93
+ * Sets the Content-Type header to application/json and stringifies the data.
94
+ *
95
+ * @function sendJson
96
+ * @param {http.ServerResponse} res - The HTTP response object
97
+ * @param {number} statusCode - HTTP status code (e.g., 200, 400, 500)
98
+ * @param {*} data - Data to JSON-stringify and send
99
+ *
100
+ * @example
101
+ * // Success response
102
+ * sendJson(res, 200, { users: [...] })
103
+ *
104
+ * @example
105
+ * // Error response
106
+ * sendJson(res, 404, { error: 'User not found' })
107
+ *
108
+ * @example
109
+ * // Validation error
110
+ * sendJson(res, 400, { error: 'Invalid input', fields: ['email'] })
111
+ */
112
+ function sendJson(res, statusCode, data) {
113
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
114
+ res.end(JSON.stringify(data));
115
+ }
116
+
117
+ /**
118
+ * Extracts a cookie value from request headers.
119
+ *
120
+ * Supports both Node.js style headers (object) and Fetch API style headers
121
+ * (Headers object with .get() method).
122
+ *
123
+ * @function getCookie
124
+ * @param {Object|Headers} headers - Request headers object
125
+ * @param {string} name - Cookie name to extract
126
+ * @returns {string|null} Cookie value, or null if not found
127
+ *
128
+ * @example
129
+ * // Node.js style headers
130
+ * const sessionId = getCookie(req.headers, 'sessionId')
131
+ *
132
+ * @example
133
+ * // Fetch API style headers
134
+ * const sessionId = getCookie(request.headers, 'sessionId')
135
+ *
136
+ * @example
137
+ * // Cookie not present
138
+ * const missing = getCookie(req.headers, 'nonexistent')
139
+ * // Returns: null
140
+ */
141
+ function getCookie(headers, name) {
142
+ // Support both Node.js style and Fetch API Headers
143
+ const cookies =
144
+ typeof headers.get === "function" ? headers.get("cookie") : headers.cookie;
145
+
146
+ if (!cookies) return null;
147
+
148
+ // Match cookie name followed by = and capture value until ; or end
149
+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
150
+ return match ? match[1] : null;
151
+ }
152
+
153
+ /**
154
+ * Checks if the host is localhost.
155
+ *
156
+ * Used to allow certain operations (like non-HTTPS file transfers)
157
+ * during local development.
158
+ *
159
+ * Recognized localhost values:
160
+ * - 'localhost'
161
+ * - '127.0.0.1'
162
+ * - '[::1]' (IPv6 localhost)
163
+ *
164
+ * @function isLocalhost
165
+ * @param {string} host - Host header value (may include port)
166
+ * @returns {boolean} True if the host is localhost
167
+ *
168
+ * @example
169
+ * isLocalhost('localhost:3000') // true
170
+ * isLocalhost('127.0.0.1:8080') // true
171
+ * isLocalhost('[::1]:3000') // true
172
+ * isLocalhost('example.com') // false
173
+ * isLocalhost('192.168.1.1:3000') // false
174
+ */
175
+ function isLocalhost(host) {
176
+ const hostname = host?.split(":")[0] || "";
177
+ return ["localhost", "127.0.0.1", "[::1]"].includes(hostname);
178
+ }
179
+
180
+ /**
181
+ * Checks if the request is using HTTPS (secure connection).
182
+ *
183
+ * Checks multiple indicators:
184
+ * 1. X-Forwarded-Proto header (for reverse proxies like nginx)
185
+ * 2. Socket encryption (direct HTTPS)
186
+ *
187
+ * Supports both Node.js request objects and Fetch API Request objects.
188
+ *
189
+ * @function isSecure
190
+ * @param {http.IncomingMessage|Request} req - The HTTP request object
191
+ * @returns {boolean} True if the connection is secure
192
+ *
193
+ * @example
194
+ * // Check if HTTPS is required
195
+ * if (!isSecure(req) && !isLocalhost(req.headers.host)) {
196
+ * return sendJson(res, 403, { error: 'HTTPS required' })
197
+ * }
198
+ *
199
+ * @example
200
+ * // Behind a reverse proxy
201
+ * // Request with header: X-Forwarded-Proto: https
202
+ * isSecure(req) // true
203
+ */
204
+ function isSecure(req) {
205
+ // Fetch API style (Headers object)
206
+ if (typeof req.headers?.get === "function") {
207
+ return req.headers.get("x-forwarded-proto") === "https";
208
+ }
209
+
210
+ // Node.js style - check socket encryption or X-Forwarded-Proto
211
+ return (
212
+ req.socket?.encrypted || req.headers?.["x-forwarded-proto"] === "https"
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Serves the api-ape client JavaScript bundle.
218
+ *
219
+ * Reads the pre-built client bundle from the dist directory and sends it
220
+ * with the appropriate Content-Type header.
221
+ *
222
+ * @function serveClientBundle
223
+ * @param {string} clientPath - The request path (used for error logging)
224
+ * @param {http.ServerResponse} res - The HTTP response object
225
+ *
226
+ * @example
227
+ * // In request handler
228
+ * if (pathname === '/api/ape.js') {
229
+ * return serveClientBundle('/api/ape.js', res)
230
+ * }
231
+ */
232
+ /* istanbul ignore next 11 - client bundle serving only used by browser clients */
233
+ function serveClientBundle(clientPath, res) {
234
+ const filePath = path.join(__dirname, "../../dist/ape.js");
235
+
236
+ fs.readFile(filePath, (err, data) => {
237
+ if (err) {
238
+ sendJson(res, 500, { error: "Failed to read client bundle" });
239
+ return;
240
+ }
241
+ res.writeHead(200, { "Content-Type": "application/javascript" });
242
+ res.end(data);
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Serves the api-ape client source map.
248
+ *
249
+ * Reads the source map file from the dist directory for debugging support.
250
+ * Returns 404 if the source map doesn't exist.
251
+ *
252
+ * @function serveSourceMap
253
+ * @param {http.ServerResponse} res - The HTTP response object
254
+ *
255
+ * @example
256
+ * // In request handler
257
+ * if (pathname === '/api/ape.js.map') {
258
+ * return serveSourceMap(res)
259
+ * }
260
+ */
261
+ /* istanbul ignore next 11 - source map serving only used by browser clients */
262
+ function serveSourceMap(res) {
263
+ const filePath = path.join(__dirname, "../../dist/ape.js.map");
264
+
265
+ fs.readFile(filePath, (err, data) => {
266
+ if (err) {
267
+ sendJson(res, 404, { error: "Source map not found" });
268
+ return;
269
+ }
270
+ res.writeHead(200, { "Content-Type": "application/json" });
271
+ res.end(data);
272
+ });
273
+ }
274
+
275
+ module.exports = {
276
+ matchRoute,
277
+ sendJson,
278
+ getCookie,
279
+ isLocalhost,
280
+ isSecure,
281
+ serveClientBundle,
282
+ serveSourceMap,
283
+ };
@@ -1,10 +1,211 @@
1
- const deeprequire = require('../utils/deepRequire')
2
- const path = require('path')
1
+ /**
2
+ * @fileoverview Controller Loader for api-ape Server
3
+ *
4
+ * This module provides the functionality to automatically load API controller
5
+ * files from a directory structure. Controllers are JavaScript files that
6
+ * export functions to handle specific API endpoints.
7
+ *
8
+ * ## How It Works
9
+ *
10
+ * The loader recursively scans a directory and loads all JavaScript files,
11
+ * mapping their file paths to endpoint names:
12
+ *
13
+ * ```
14
+ * Directory Structure: Endpoint Mapping:
15
+ * ───────────────────── ─────────────────
16
+ * api/
17
+ * ├── users.js → controllers['users']
18
+ * ├── users/
19
+ * │ ├── profile.js → controllers['users/profile']
20
+ * │ └── settings.js → controllers['users/settings']
21
+ * ├── chat.js → controllers['chat']
22
+ * └── admin/
23
+ * └── dashboard.js → controllers['admin/dashboard']
24
+ * ```
25
+ *
26
+ * ## Controller File Format
27
+ *
28
+ * Each controller file should export a function (sync or async) that handles
29
+ * requests to that endpoint:
30
+ *
31
+ * ```javascript
32
+ * // api/users.js
33
+ * module.exports = async function(data) {
34
+ * // `this` contains embed values from onConnect
35
+ * const { userId, permissions } = this
36
+ *
37
+ * // `data` is the payload sent by the client
38
+ * const { action, query } = data
39
+ *
40
+ * // Return value is sent back to client
41
+ * return await db.users.find(query)
42
+ * }
43
+ * ```
44
+ *
45
+ * ## Index Files
46
+ *
47
+ * Files named `index.js` are mapped to their parent directory:
48
+ * - `api/users/index.js` → `controllers['users']`
49
+ *
50
+ * ## Duplicate Detection
51
+ *
52
+ * The loader detects duplicate endpoints and throws an error:
53
+ * - `api/users.js` and `api/users/index.js` would both map to 'users'
54
+ * - This is caught and reported with both file paths
55
+ *
56
+ * @module server/lib/loader
57
+ * @see {@link module:server/utils/deepRequire} for the underlying loader implementation
58
+ * @see {@link module:server/lib/main} for how controllers are used
59
+ *
60
+ * @example <caption>Basic Usage</caption>
61
+ * const loader = require('./loader')
62
+ *
63
+ * // Load all controllers from ./api directory
64
+ * const controllers = loader('api')
65
+ *
66
+ * // controllers = {
67
+ * // 'users': [Function],
68
+ * // 'users/profile': [Function],
69
+ * // 'chat': [Function],
70
+ * // 'admin/dashboard': [Function]
71
+ * // }
72
+ *
73
+ * @example <caption>Calling a Controller</caption>
74
+ * const controllers = loader('api')
75
+ *
76
+ * // This is how api-ape invokes controllers internally
77
+ * const handler = controllers['users']
78
+ * const context = { userId: 123, permissions: ['read'] }
79
+ * const result = await handler.call(context, { action: 'list' })
80
+ *
81
+ * @example <caption>Controller Implementation</caption>
82
+ * // api/messages.js
83
+ * module.exports = async function(data) {
84
+ * // Available context via `this`:
85
+ * // - this.clientId - Unique client identifier
86
+ * // - this.sessionId - Session ID from cookies
87
+ * // - this.req - Original HTTP request (WebSocket upgrade)
88
+ * // - this.send - Function to send messages to this client
89
+ * // - this.broadcast - Function to broadcast to all clients
90
+ * // - this.broadcastOthers - Broadcast excluding this client
91
+ * // - this.clients - Map of all connected clients
92
+ * // - ...embed values - Custom values from onConnect
93
+ *
94
+ * const { roomId, text } = data
95
+ *
96
+ * // Save to database
97
+ * const message = await db.messages.create({
98
+ * roomId,
99
+ * text,
100
+ * userId: this.userId, // From embed
101
+ * createdAt: new Date()
102
+ * })
103
+ *
104
+ * // Broadcast to other users in the room
105
+ * this.broadcastOthers('new-message', {
106
+ * roomId,
107
+ * message
108
+ * })
109
+ *
110
+ * return message
111
+ * }
112
+ *
113
+ * @example <caption>Nested Routes</caption>
114
+ * // api/admin/users/ban.js → endpoint: 'admin/users/ban'
115
+ * module.exports = async function({ userId, reason }) {
116
+ * // Check admin permissions
117
+ * if (!this.permissions?.includes('admin')) {
118
+ * throw new Error('Permission denied')
119
+ * }
120
+ *
121
+ * await db.users.ban(userId, reason)
122
+ * return { success: true }
123
+ * }
124
+ */
3
125
 
4
- // Use the current working directory (where node was started)
5
- // This ensures 'where' folder is relative to the calling application
6
- const currentDir = process.cwd()
126
+ const deeprequire = require("../utils/deepRequire");
127
+ const path = require("path");
7
128
 
129
+ /**
130
+ * Current working directory where Node.js was started
131
+ *
132
+ * This ensures that the 'where' folder path is resolved relative to
133
+ * the application root, not relative to this module's location.
134
+ *
135
+ * @constant {string}
136
+ * @private
137
+ */
138
+ const currentDir = process.cwd();
139
+
140
+ /**
141
+ * Load all controller files from a directory
142
+ *
143
+ * Recursively scans the specified directory for JavaScript files and
144
+ * loads them as controller functions. Each file's path (relative to the
145
+ * directory) becomes its endpoint name.
146
+ *
147
+ * ## Path Resolution
148
+ *
149
+ * The `dirname` parameter is resolved relative to `process.cwd()` (where
150
+ * Node.js was started), not relative to this module. This allows the
151
+ * calling application to specify paths relative to its own root.
152
+ *
153
+ * ## File Selection
154
+ *
155
+ * By default, all `.js` files are loaded. Use the optional `selector`
156
+ * parameter to customize which files are included.
157
+ *
158
+ * ## Error Handling
159
+ *
160
+ * - Throws if `dirname` doesn't exist
161
+ * - Throws if duplicate endpoints are detected (e.g., `users.js` and `users/index.js`)
162
+ * - Throws if a controller file has syntax errors
163
+ *
164
+ * @param {string} dirname - Directory name relative to current working directory
165
+ * (e.g., 'api', 'controllers', 'src/api')
166
+ * @param {string[]} [selector=['js']] - File extensions to include (without dots)
167
+ * @returns {Object.<string, Function>} Object mapping endpoint paths to controller functions
168
+ * @throws {Error} If directory doesn't exist or contains duplicate endpoints
169
+ *
170
+ * @example
171
+ * // Load from ./api directory
172
+ * const controllers = loader('api')
173
+ *
174
+ * @example
175
+ * // Load from nested directory
176
+ * const controllers = loader('src/api/v1')
177
+ *
178
+ * @example
179
+ * // Custom file extension selector
180
+ * const controllers = loader('api', ['js', 'mjs'])
181
+ *
182
+ * @example
183
+ * // Resulting controller object structure
184
+ * const controllers = loader('api')
185
+ * // {
186
+ * // 'users': function(data) { ... },
187
+ * // 'users/profile': function(data) { ... },
188
+ * // 'users/settings': function(data) { ... },
189
+ * // 'chat': function(data) { ... },
190
+ * // 'chat/rooms': function(data) { ... },
191
+ * // 'admin/stats': function(data) { ... }
192
+ * // }
193
+ *
194
+ * @example
195
+ * // Manual controller invocation (internal use)
196
+ * const controllers = loader('api')
197
+ *
198
+ * async function handleRequest(endpoint, data, context) {
199
+ * const handler = controllers[endpoint]
200
+ *
201
+ * if (!handler) {
202
+ * throw new Error(`Endpoint not found: ${endpoint}`)
203
+ * }
204
+ *
205
+ * // Call with context bound to `this`
206
+ * return await handler.call(context, data)
207
+ * }
208
+ */
8
209
  module.exports = function (dirname, selector) {
9
- return deeprequire(path.join(currentDir, dirname), selector)
10
- }
210
+ return deeprequire(path.join(currentDir, dirname), selector);
211
+ };
@@ -0,0 +1,63 @@
1
+ # Long Polling Module
2
+
3
+ ## Overview
4
+
5
+ The longPolling module provides HTTP-based fallback communication when WebSocket connections are unavailable. Many corporate networks, firewalls, and proxy servers block WebSocket connections, so api-ape automatically falls back to HTTP long-polling to maintain real-time bidirectional communication.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **Streaming responses** — Server holds GET requests open and streams JSON events as they occur
10
+ - **Message posting** — Client sends messages via POST requests with immediate responses
11
+ - **Session management** — Tracks clients via `apeClientId` cookie across requests
12
+ - **Heartbeat keepalive** — Prevents proxy timeouts with periodic heartbeat messages
13
+ - **Connection recycling** — Automatically closes and reopens connections every ~25 seconds
14
+ - **Broadcast integration** — Long-polling clients receive broadcasts just like WebSocket clients
15
+
16
+ The transport is transparent to application code—controllers work identically regardless of whether clients connect via WebSocket or HTTP long-polling.
17
+
18
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
19
+
20
+ ## How It Works
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────────────────────────┐
24
+ │ Client │
25
+ ├─────────────────────────────────────────────────────────────┤
26
+ │ │
27
+ │ GET /api/ape/poll ──────────────────► Streaming Response │
28
+ │ (reconnects every ~25s) ◄── JSON events │
29
+ │ ◄── Heartbeats │
30
+ │ │
31
+ │ POST /api/ape/poll ─────────────────► Request/Response │
32
+ │ { type: '/users/list', data: {} } │
33
+ │ ◄── { data: [...] } │
34
+ │ │
35
+ └─────────────────────────────────────────────────────────────┘
36
+ ```
37
+
38
+ ## Response Headers
39
+
40
+ The GET handler sets these headers for proper streaming:
41
+
42
+ ```
43
+ Content-Type: application/json
44
+ Cache-Control: no-cache
45
+ Connection: keep-alive
46
+ X-Accel-Buffering: no # Disables nginx/proxy buffering
47
+ ```
48
+
49
+ ## When Is This Used?
50
+
51
+ Long-polling activates automatically when:
52
+
53
+ 1. WebSocket connection fails to establish
54
+ 2. WebSocket upgrade is rejected by a proxy or firewall
55
+ 3. Network doesn't support the WebSocket protocol
56
+ 4. Client explicitly requests HTTP transport
57
+
58
+ ## See Also
59
+
60
+ - [`../longPolling.js`](../longPolling.js) — Main long-polling coordinator
61
+ - [`../wiring.js`](../wiring.js) — WebSocket wiring (primary transport)
62
+ - [`../broadcast.js`](../broadcast.js) — Client tracking for long-polling clients
63
+ - [`../../../client/transports/README.md`](../../../client/transports/README.md) — Client-side streaming transport