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,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
|
+
};
|
package/server/lib/loader.js
CHANGED
|
@@ -1,10 +1,211 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|