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
package/server/lib/main.js
CHANGED
|
@@ -1,561 +1,398 @@
|
|
|
1
|
-
const loader = require('./loader')
|
|
2
|
-
const wiring = require('./wiring')
|
|
3
|
-
const { getWebSocketProvider, isBun, isDeno, getRuntime } = require('./wsProvider')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const fs = require('fs')
|
|
6
|
-
const { getFileTransferManager } = require('./fileTransfer')
|
|
7
|
-
const { createLongPollingHandler } = require('./longPolling')
|
|
8
|
-
const { parse: parseUrl } = require('url')
|
|
9
|
-
|
|
10
|
-
let created = false
|
|
11
|
-
|
|
12
1
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
2
|
+
* @fileoverview Main api-ape Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This module provides the unified entry point for initializing api-ape on any
|
|
5
|
+
* supported server platform. It handles runtime detection, WebSocket setup,
|
|
6
|
+
* controller loading, and HTTP endpoint configuration.
|
|
7
|
+
*
|
|
8
|
+
* ## Supported Runtimes
|
|
9
|
+
*
|
|
10
|
+
* | Runtime | WebSocket Provider | Notes |
|
|
11
|
+
* |-----------|---------------------------|------------------------------|
|
|
12
|
+
* | Node.js | Polyfill or native (v24+) | Full feature support |
|
|
13
|
+
* | Bun | Native Bun WebSocket | High performance |
|
|
14
|
+
* | Deno | Native Deno WebSocket | Via adapter |
|
|
15
|
+
*
|
|
16
|
+
* ## Initialization Flow
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* ape(server, options)
|
|
20
|
+
* │
|
|
21
|
+
* ▼
|
|
22
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
23
|
+
* │ createApeCore(options) │
|
|
24
|
+
* │ ├── Load controllers from 'where' directory │
|
|
25
|
+
* │ ├── Initialize file transfer manager │
|
|
26
|
+
* │ ├── Create WebSocket wiring handler │
|
|
27
|
+
* │ └── Create HTTP long-polling handlers │
|
|
28
|
+
* └─────────────────────────────────────────────────────┘
|
|
29
|
+
* │
|
|
30
|
+
* ▼
|
|
31
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
32
|
+
* │ Detect Server Type │
|
|
33
|
+
* │ ├── Bun.serve() server → initBunServerWithReload │
|
|
34
|
+
* │ └── Node.js http.Server → initNodeServer │
|
|
35
|
+
* └─────────────────────────────────────────────────────┘
|
|
36
|
+
* │
|
|
37
|
+
* ▼
|
|
38
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
39
|
+
* │ Server Running │
|
|
40
|
+
* │ ├── WebSocket: /api/ape (or custom path) │
|
|
41
|
+
* │ ├── Long-poll: /api/ape/poll │
|
|
42
|
+
* │ ├── Ping: /api/ape/ping │
|
|
43
|
+
* │ ├── Client bundle: /api/ape.js │
|
|
44
|
+
* │ └── File transfer: /api/ape/data/:hash │
|
|
45
|
+
* └─────────────────────────────────────────────────────┘
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* ## HTTP Endpoints Created
|
|
49
|
+
*
|
|
50
|
+
* | Endpoint | Method | Description |
|
|
51
|
+
* |-----------------------------|--------|--------------------------------|
|
|
52
|
+
* | `/{where}/ape` | WS | WebSocket connection endpoint |
|
|
53
|
+
* | `/{where}/ape/poll` | GET | HTTP streaming (long-poll) |
|
|
54
|
+
* | `/{where}/ape/poll` | POST | Send message via HTTP |
|
|
55
|
+
* | `/{where}/ape/ping` | GET | Connection health check |
|
|
56
|
+
* | `/{where}/ape.js` | GET | Client JavaScript bundle |
|
|
57
|
+
* | `/{where}/ape.js.map` | GET | Source map for debugging |
|
|
58
|
+
* | `/{where}/ape/data/:hash` | GET | Download binary data |
|
|
59
|
+
* | `/{where}/ape/data/:qid/:h` | PUT | Upload binary data |
|
|
60
|
+
*
|
|
61
|
+
* @module server/lib/main
|
|
62
|
+
* @see {@link module:server/lib/wiring} for WebSocket connection handling
|
|
63
|
+
* @see {@link module:server/lib/loader} for controller loading
|
|
64
|
+
* @see {@link module:server/lib/longPolling} for HTTP fallback
|
|
65
|
+
*
|
|
66
|
+
* @example <caption>Basic Node.js Setup</caption>
|
|
67
|
+
* const http = require('http')
|
|
68
|
+
* const ape = require('api-ape/server/lib/main')
|
|
69
|
+
*
|
|
70
|
+
* const server = http.createServer()
|
|
71
|
+
*
|
|
72
|
+
* ape(server, {
|
|
73
|
+
* where: 'api', // Load controllers from ./api directory
|
|
74
|
+
* onConnect: (socket, req, send) => ({
|
|
75
|
+
* embed: { userId: getUserFromRequest(req) }
|
|
76
|
+
* })
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* server.listen(3000)
|
|
80
|
+
*
|
|
81
|
+
* @example <caption>Express Integration</caption>
|
|
82
|
+
* const express = require('express')
|
|
83
|
+
* const ape = require('api-ape/server/lib/main')
|
|
84
|
+
*
|
|
85
|
+
* const app = express()
|
|
86
|
+
* const server = app.listen(3000)
|
|
87
|
+
*
|
|
88
|
+
* ape(server, {
|
|
89
|
+
* where: 'api',
|
|
90
|
+
* onConnect: async (socket, req, send) => {
|
|
91
|
+
* const session = await validateSession(req)
|
|
92
|
+
* return {
|
|
93
|
+
* embed: { user: session.user, permissions: session.permissions },
|
|
94
|
+
* onDisconnect: () => logDisconnect(session.user)
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* })
|
|
98
|
+
*
|
|
99
|
+
* @example <caption>Bun Server</caption>
|
|
100
|
+
* const ape = require('api-ape/server/lib/main')
|
|
101
|
+
*
|
|
102
|
+
* const server = Bun.serve({
|
|
103
|
+
* port: 3000,
|
|
104
|
+
* fetch(req) { return new Response('Hello') },
|
|
105
|
+
* websocket: { message() {} } // Required for api-ape
|
|
106
|
+
* })
|
|
107
|
+
*
|
|
108
|
+
* ape(server, { where: 'api' })
|
|
15
109
|
*/
|
|
16
|
-
function matchRoute(pathname, pattern) {
|
|
17
|
-
const patternParts = pattern.split('/')
|
|
18
|
-
const pathParts = pathname.split('/')
|
|
19
|
-
|
|
20
|
-
if (patternParts.length !== pathParts.length) {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
110
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return params
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Send JSON response (Node.js style)
|
|
37
|
-
*/
|
|
38
|
-
function sendJson(res, statusCode, data) {
|
|
39
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' })
|
|
40
|
-
res.end(JSON.stringify(data))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get cookie value from request headers
|
|
45
|
-
*/
|
|
46
|
-
function getCookie(headers, name) {
|
|
47
|
-
const cookies = typeof headers.get === 'function'
|
|
48
|
-
? headers.get('cookie')
|
|
49
|
-
: headers.cookie
|
|
50
|
-
if (!cookies) return null
|
|
51
|
-
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
|
52
|
-
return match ? match[1] : null
|
|
53
|
-
}
|
|
111
|
+
const loader = require("./loader");
|
|
112
|
+
const wiring = require("./wiring");
|
|
113
|
+
const { isBun, isDeno, getRuntime } = require("./wsProvider");
|
|
114
|
+
const { getFileTransferManager } = require("./fileTransfer");
|
|
115
|
+
const { createLongPollingHandler } = require("./longPolling");
|
|
116
|
+
const { initNodeServer } = require("./runtimes/node");
|
|
117
|
+
const { isBunServer, initBunServerWithReload } = require("./runtimes/bun");
|
|
54
118
|
|
|
55
119
|
/**
|
|
56
|
-
*
|
|
120
|
+
* Flag to track whether api-ape has been initialized
|
|
121
|
+
*
|
|
122
|
+
* Prevents multiple initializations which could cause conflicts
|
|
123
|
+
* with WebSocket handlers and HTTP routes.
|
|
124
|
+
*
|
|
125
|
+
* @type {boolean}
|
|
126
|
+
* @private
|
|
57
127
|
*/
|
|
58
|
-
|
|
59
|
-
const hostname = host?.split(':')[0] || ''
|
|
60
|
-
return ['localhost', '127.0.0.1', '[::1]'].includes(hostname)
|
|
61
|
-
}
|
|
128
|
+
let created = false;
|
|
62
129
|
|
|
63
130
|
/**
|
|
64
|
-
*
|
|
131
|
+
* Reset the singleton state for testing purposes.
|
|
132
|
+
* This allows creating multiple server instances in test environments.
|
|
133
|
+
*
|
|
134
|
+
* @private
|
|
135
|
+
* @function _resetForTesting
|
|
65
136
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return req.headers.get('x-forwarded-proto') === 'https'
|
|
69
|
-
}
|
|
70
|
-
return req.socket?.encrypted || req.headers?.['x-forwarded-proto'] === 'https'
|
|
137
|
+
function _resetForTesting() {
|
|
138
|
+
created = false;
|
|
71
139
|
}
|
|
72
140
|
|
|
73
141
|
/**
|
|
74
|
-
* Create core api-ape handlers
|
|
142
|
+
* Create the core api-ape handlers shared between all runtimes
|
|
143
|
+
*
|
|
144
|
+
* This function initializes the components needed regardless of the
|
|
145
|
+
* server runtime (Node.js, Bun, Deno):
|
|
146
|
+
* - Controller loading from the specified directory
|
|
147
|
+
* - File transfer manager for binary data
|
|
148
|
+
* - WebSocket message handler (wiring)
|
|
149
|
+
* - HTTP long-polling handlers for fallback transport
|
|
150
|
+
* - URL path patterns for all endpoints
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} options - Configuration options
|
|
153
|
+
* @param {string} options.where - Directory containing API controllers (relative to CWD)
|
|
154
|
+
* @param {Function} [options.onConnect] - Connection lifecycle callback
|
|
155
|
+
* @param {Object} [options.fileTransferOptions] - File transfer configuration
|
|
156
|
+
* @param {number} [options.fileTransferOptions.startTimeout=60000] - Timeout before upload starts
|
|
157
|
+
* @param {number} [options.fileTransferOptions.completeTimeout=60000] - Timeout for upload completion
|
|
158
|
+
* @param {Object} [options.longPollingOptions] - Long polling configuration
|
|
159
|
+
* @param {number} [options.longPollingOptions.heartbeatInterval=20000] - Interval in ms for heartbeat pings
|
|
160
|
+
* @param {number} [options.longPollingOptions.recycleTimeout=25000] - Timeout in ms before recycling connection
|
|
161
|
+
* @returns {ApeCore} Core handlers and path patterns
|
|
162
|
+
*
|
|
163
|
+
* @typedef {Object} ApeCore
|
|
164
|
+
* @property {Object} controllers - Loaded controller functions keyed by endpoint path
|
|
165
|
+
* @property {FileTransferManager} fileTransfer - Binary data transfer manager
|
|
166
|
+
* @property {Function} wiringHandler - WebSocket connection handler
|
|
167
|
+
* @property {Function} handleStreamGet - HTTP GET handler for long-polling
|
|
168
|
+
* @property {Function} handleStreamPost - HTTP POST handler for sending messages
|
|
169
|
+
* @property {string} wsPath - WebSocket endpoint path (e.g., '/api/ape')
|
|
170
|
+
* @property {string} pollPath - Long-polling endpoint path
|
|
171
|
+
* @property {string} pingPath - Health check endpoint path
|
|
172
|
+
* @property {string} clientPath - Client bundle endpoint path
|
|
173
|
+
* @property {string} clientMapPath - Source map endpoint path
|
|
174
|
+
* @property {string} downloadPattern - Binary download URL pattern
|
|
175
|
+
* @property {string} uploadPattern - Binary upload URL pattern
|
|
176
|
+
*
|
|
177
|
+
* @private
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* const core = createApeCore({
|
|
181
|
+
* where: 'api',
|
|
182
|
+
* onConnect: myConnectHandler,
|
|
183
|
+
* fileTransferOptions: { startTimeout: 30000 }
|
|
184
|
+
* })
|
|
185
|
+
*
|
|
186
|
+
* // core.controllers = { 'users': [Function], 'chat': [Function], ... }
|
|
187
|
+
* // core.wsPath = '/api/ape'
|
|
75
188
|
*/
|
|
76
|
-
function createApeCore({ where, onConnect, fileTransferOptions }) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
downloadPattern,
|
|
102
|
-
uploadPattern
|
|
103
|
-
}
|
|
189
|
+
function createApeCore({ where, onConnect, fileTransferOptions, longPollingOptions }) {
|
|
190
|
+
const controllers = loader(where);
|
|
191
|
+
const fileTransfer = getFileTransferManager(fileTransferOptions);
|
|
192
|
+
const wiringHandler = wiring(controllers, onConnect, fileTransfer);
|
|
193
|
+
const { handleStreamGet, handleStreamPost } = createLongPollingHandler(
|
|
194
|
+
controllers,
|
|
195
|
+
onConnect,
|
|
196
|
+
fileTransfer,
|
|
197
|
+
longPollingOptions,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
controllers,
|
|
202
|
+
fileTransfer,
|
|
203
|
+
wiringHandler,
|
|
204
|
+
handleStreamGet,
|
|
205
|
+
handleStreamPost,
|
|
206
|
+
wsPath: `/${where}/ape`,
|
|
207
|
+
pollPath: `/${where}/ape/poll`,
|
|
208
|
+
pingPath: `/${where}/ape/ping`,
|
|
209
|
+
clientPath: `/${where}/ape.js`,
|
|
210
|
+
clientMapPath: `/${where}/ape.js.map`,
|
|
211
|
+
downloadPattern: `/${where}/ape/data/:hash`,
|
|
212
|
+
uploadPattern: `/${where}/ape/data/:queryId/:pathHash`,
|
|
213
|
+
};
|
|
104
214
|
}
|
|
105
215
|
|
|
106
216
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
217
|
+
* Initialize api-ape on an HTTP server
|
|
218
|
+
*
|
|
219
|
+
* This is the main entry point for setting up api-ape. It:
|
|
220
|
+
* 1. Validates that api-ape hasn't already been initialized
|
|
221
|
+
* 2. Creates core handlers (controllers, WebSocket, HTTP)
|
|
222
|
+
* 3. Detects the server type (Node.js, Bun)
|
|
223
|
+
* 4. Initializes the appropriate runtime-specific handlers
|
|
224
|
+
*
|
|
225
|
+
* ## Options
|
|
226
|
+
*
|
|
227
|
+
* | Option | Type | Description |
|
|
228
|
+
* |----------------------|----------|--------------------------------------------|
|
|
229
|
+
* | `where` | string | Directory containing API controllers |
|
|
230
|
+
* | `onConnect` | Function | Called when a client connects |
|
|
231
|
+
* | `fileTransferOptions`| Object | Binary file transfer configuration |
|
|
232
|
+
* | `transport` | string | Force transport: 'websocket' or 'longpolling' |
|
|
233
|
+
*
|
|
234
|
+
* ## onConnect Callback
|
|
235
|
+
*
|
|
236
|
+
* The `onConnect` function is called for each new WebSocket connection.
|
|
237
|
+
* It receives `(socket, req, send)` and should return an options object:
|
|
238
|
+
*
|
|
239
|
+
* ```javascript
|
|
240
|
+
* onConnect: (socket, req, send) => ({
|
|
241
|
+
* embed: { userId: '123' }, // Values available in all controllers as `this`
|
|
242
|
+
* onReceive: (queryId, data, type) => { }, // Called when message received
|
|
243
|
+
* onSend: (data, type) => { }, // Called when message sent
|
|
244
|
+
* onError: (errorString) => { }, // Called on errors
|
|
245
|
+
* onDisconnect: () => { } // Called when client disconnects
|
|
246
|
+
* })
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @param {http.Server|Object} server - HTTP server instance (Node.js or Bun)
|
|
250
|
+
* @param {Object} options - Configuration options
|
|
251
|
+
* @param {string} options.where - Directory containing API controller files
|
|
252
|
+
* @param {Function} [options.onConnect] - Connection lifecycle callback
|
|
253
|
+
* @param {Object} [options.fileTransferOptions] - File transfer settings
|
|
254
|
+
* @param {string} [options.transport] - Force specific transport mode
|
|
255
|
+
* @returns {Object} Server information including WebSocket server instance
|
|
256
|
+
* @throws {Error} If api-ape has already been initialized
|
|
257
|
+
* @throws {Error} If server type is not supported
|
|
258
|
+
*
|
|
259
|
+
* @example <caption>Minimal Setup</caption>
|
|
260
|
+
* const http = require('http')
|
|
261
|
+
* const ape = require('api-ape/server/lib/main')
|
|
262
|
+
*
|
|
263
|
+
* const server = http.createServer()
|
|
264
|
+
* ape(server, { where: 'api' })
|
|
265
|
+
* server.listen(3000)
|
|
266
|
+
*
|
|
267
|
+
* @example <caption>Full Configuration</caption>
|
|
268
|
+
* ape(server, {
|
|
269
|
+
* where: 'api',
|
|
270
|
+
*
|
|
271
|
+
* onConnect: async (socket, req, send) => {
|
|
272
|
+
* // Authenticate user from request
|
|
273
|
+
* const token = req.headers.cookie?.match(/token=([^;]+)/)?.[1]
|
|
274
|
+
* const user = await verifyToken(token)
|
|
275
|
+
*
|
|
276
|
+
* if (!user) {
|
|
277
|
+
* socket.close(4001, 'Unauthorized')
|
|
278
|
+
* return null
|
|
279
|
+
* }
|
|
280
|
+
*
|
|
281
|
+
* // Send welcome message
|
|
282
|
+
* send('welcome', { message: `Hello, ${user.name}!` })
|
|
283
|
+
*
|
|
284
|
+
* return {
|
|
285
|
+
* embed: {
|
|
286
|
+
* user,
|
|
287
|
+
* permissions: user.roles
|
|
288
|
+
* },
|
|
289
|
+
* onReceive: (queryId, data, type) => {
|
|
290
|
+
* console.log(`[${user.name}] ${type}:`, data)
|
|
291
|
+
* },
|
|
292
|
+
* onDisconnect: () => {
|
|
293
|
+
* console.log(`[${user.name}] disconnected`)
|
|
294
|
+
* }
|
|
295
|
+
* }
|
|
296
|
+
* },
|
|
297
|
+
*
|
|
298
|
+
* fileTransferOptions: {
|
|
299
|
+
* startTimeout: 30000,
|
|
300
|
+
* completeTimeout: 120000
|
|
301
|
+
* }
|
|
302
|
+
* })
|
|
303
|
+
*
|
|
304
|
+
* @example <caption>Controller File Structure</caption>
|
|
305
|
+
* // api/users.js - handles /users endpoint
|
|
306
|
+
* module.exports = async function(data) {
|
|
307
|
+
* // `this` contains embed values from onConnect
|
|
308
|
+
* const { user, permissions } = this
|
|
309
|
+
*
|
|
310
|
+
* if (!permissions.includes('users:read')) {
|
|
311
|
+
* throw new Error('Permission denied')
|
|
312
|
+
* }
|
|
313
|
+
*
|
|
314
|
+
* return await db.users.find(data.query)
|
|
315
|
+
* }
|
|
316
|
+
*
|
|
317
|
+
* // api/users/profile.js - handles /users/profile endpoint
|
|
318
|
+
* module.exports = async function(data) {
|
|
319
|
+
* return await db.users.findById(this.user.id)
|
|
320
|
+
* }
|
|
109
321
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Handle HTTP requests for api-ape routes
|
|
136
|
-
server.on('request', (req, res) => {
|
|
137
|
-
const { pathname } = parseUrl(req.url)
|
|
138
|
-
|
|
139
|
-
// Serve bundled client
|
|
140
|
-
if (pathname === core.clientPath) {
|
|
141
|
-
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
142
|
-
fs.readFile(filePath, (err, data) => {
|
|
143
|
-
if (err) {
|
|
144
|
-
sendJson(res, 500, { error: 'Failed to read client bundle' })
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
148
|
-
res.end(data)
|
|
149
|
-
})
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Serve source map for debugging
|
|
154
|
-
if (pathname === core.clientMapPath) {
|
|
155
|
-
const filePath = path.join(__dirname, '../../dist/ape.js.map')
|
|
156
|
-
fs.readFile(filePath, (err, data) => {
|
|
157
|
-
if (err) {
|
|
158
|
-
sendJson(res, 404, { error: 'Source map not found' })
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
162
|
-
res.end(data)
|
|
163
|
-
})
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Ping endpoint for captive portal detection
|
|
168
|
-
if (pathname === core.pingPath && req.method === 'GET') {
|
|
169
|
-
return sendJson(res, 200, { ok: true, ts: Date.now() })
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Long polling - GET
|
|
173
|
-
if (pathname === core.pollPath && req.method === 'GET') {
|
|
174
|
-
core.handleStreamGet(req, res)
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Long polling - POST
|
|
179
|
-
if (pathname === core.pollPath && req.method === 'POST') {
|
|
180
|
-
core.handleStreamPost(req, res, core.controllers)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// File download
|
|
185
|
-
const downloadMatch = matchRoute(pathname, core.downloadPattern)
|
|
186
|
-
if (req.method === 'GET' && downloadMatch) {
|
|
187
|
-
const { hash } = downloadMatch
|
|
188
|
-
|
|
189
|
-
// Check for streaming file first (client-to-client sharing, no session check)
|
|
190
|
-
const streamingFile = core.fileTransfer.getStreamingFile(hash)
|
|
191
|
-
if (streamingFile) {
|
|
192
|
-
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
193
|
-
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Return available data (may be partial if upload still in progress)
|
|
197
|
-
res.writeHead(200, {
|
|
198
|
-
'Content-Type': 'application/octet-stream',
|
|
199
|
-
'Content-Length': streamingFile.data.length,
|
|
200
|
-
'X-Ape-Complete': streamingFile.isComplete ? '1' : '0',
|
|
201
|
-
'X-Ape-Total-Received': String(streamingFile.totalReceived)
|
|
202
|
-
})
|
|
203
|
-
res.end(streamingFile.data)
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Session-bound download (original behavior)
|
|
208
|
-
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
209
|
-
|
|
210
|
-
if (!clientId) {
|
|
211
|
-
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
215
|
-
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const result = core.fileTransfer.getDownload(hash, clientId)
|
|
219
|
-
|
|
220
|
-
if (!result) {
|
|
221
|
-
return sendJson(res, 404, { error: 'Download not found or unauthorized' })
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
res.writeHead(200, {
|
|
225
|
-
'Content-Type': result.contentType,
|
|
226
|
-
'Content-Length': result.data.length || result.data.byteLength
|
|
227
|
-
})
|
|
228
|
-
res.end(result.data)
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// File upload
|
|
233
|
-
const uploadMatch = matchRoute(pathname, core.uploadPattern)
|
|
234
|
-
if (req.method === 'PUT' && uploadMatch) {
|
|
235
|
-
const { queryId, pathHash } = uploadMatch
|
|
236
|
-
|
|
237
|
-
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
238
|
-
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const chunks = []
|
|
242
|
-
req.on('data', chunk => chunks.push(chunk))
|
|
243
|
-
req.on('end', () => {
|
|
244
|
-
const data = Buffer.concat(chunks)
|
|
245
|
-
|
|
246
|
-
// Check if this is a streaming file upload (client-to-client)
|
|
247
|
-
// For streaming files, queryId might be the fileId directly
|
|
248
|
-
if (core.fileTransfer.isStreamingFile(pathHash)) {
|
|
249
|
-
const success = core.fileTransfer.completeStreamingUpload(pathHash, data)
|
|
250
|
-
if (success) {
|
|
251
|
-
return sendJson(res, 200, { success: true, streaming: true })
|
|
252
|
-
}
|
|
253
|
-
return sendJson(res, 404, { error: 'Streaming file not found' })
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Regular upload (session-bound)
|
|
257
|
-
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
258
|
-
if (!clientId) {
|
|
259
|
-
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
|
|
263
|
-
|
|
264
|
-
if (success) {
|
|
265
|
-
sendJson(res, 200, { success: true })
|
|
266
|
-
} else {
|
|
267
|
-
sendJson(res, 404, { error: 'Upload not expected or unauthorized' })
|
|
268
|
-
}
|
|
269
|
-
})
|
|
270
|
-
req.on('error', (err) => {
|
|
271
|
-
sendJson(res, 500, { error: err.message })
|
|
272
|
-
})
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Not an api-ape route - pass to original handlers
|
|
277
|
-
for (const listener of originalListeners) {
|
|
278
|
-
listener.call(server, req, res)
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
return { wss, core }
|
|
283
|
-
}
|
|
322
|
+
module.exports = function (server, options) {
|
|
323
|
+
/* istanbul ignore next 3 - would break test isolation to test "already started" */
|
|
324
|
+
if (created) {
|
|
325
|
+
throw new Error("Api-Ape already started");
|
|
326
|
+
}
|
|
327
|
+
created = true;
|
|
328
|
+
|
|
329
|
+
const core = createApeCore(options);
|
|
330
|
+
|
|
331
|
+
// Check for Bun server first
|
|
332
|
+
/* istanbul ignore next 3 - only reachable in Bun runtime */
|
|
333
|
+
if (isBunServer(server)) {
|
|
334
|
+
return initBunServerWithReload(server, options, core);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for Node.js http.Server (or Express, Koa, etc.)
|
|
338
|
+
if (server && typeof server.on === "function") {
|
|
339
|
+
return initNodeServer(server, options, core);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* istanbul ignore next 3 - requires passing invalid server type */
|
|
343
|
+
throw new Error(
|
|
344
|
+
"Unsupported server type. Expected http.Server (Node.js) or Bun.serve() server.",
|
|
345
|
+
);
|
|
346
|
+
};
|
|
284
347
|
|
|
285
348
|
/**
|
|
286
|
-
* Bun
|
|
287
|
-
*
|
|
349
|
+
* Check if running in Bun runtime
|
|
350
|
+
*
|
|
351
|
+
* Useful for conditional logic based on runtime capabilities.
|
|
352
|
+
*
|
|
353
|
+
* @function isBun
|
|
354
|
+
* @returns {boolean} True if running in Bun
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* const { isBun } = require('api-ape/server/lib/main')
|
|
358
|
+
*
|
|
359
|
+
* if (isBun()) {
|
|
360
|
+
* console.log('Running on Bun - native WebSocket available')
|
|
361
|
+
* }
|
|
288
362
|
*/
|
|
289
|
-
|
|
290
|
-
const { BunWebSocket } = require('./ws/adapters/bun')
|
|
291
|
-
const core = createApeCore(options)
|
|
292
|
-
const clients = new Map()
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Fetch handler for Bun.serve()
|
|
296
|
-
* Handles all api-ape routes, returns null for non-ape routes
|
|
297
|
-
*/
|
|
298
|
-
function fetch(req, server) {
|
|
299
|
-
const url = new URL(req.url)
|
|
300
|
-
const pathname = url.pathname
|
|
301
|
-
|
|
302
|
-
// WebSocket upgrade
|
|
303
|
-
if (pathname === core.wsPath) {
|
|
304
|
-
const upgrade = req.headers.get('upgrade')
|
|
305
|
-
if (upgrade?.toLowerCase() === 'websocket') {
|
|
306
|
-
const success = server.upgrade(req, { data: { req } })
|
|
307
|
-
if (success) return undefined
|
|
308
|
-
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Serve client bundle
|
|
313
|
-
if (pathname === core.clientPath) {
|
|
314
|
-
try {
|
|
315
|
-
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
316
|
-
const data = fs.readFileSync(filePath)
|
|
317
|
-
return new Response(data, {
|
|
318
|
-
headers: { 'Content-Type': 'application/javascript' }
|
|
319
|
-
})
|
|
320
|
-
} catch {
|
|
321
|
-
return new Response('Client bundle not found', { status: 500 })
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Serve source map for debugging
|
|
326
|
-
if (pathname === core.clientMapPath) {
|
|
327
|
-
try {
|
|
328
|
-
const filePath = path.join(__dirname, '../../dist/ape.js.map')
|
|
329
|
-
const data = fs.readFileSync(filePath)
|
|
330
|
-
return new Response(data, {
|
|
331
|
-
headers: { 'Content-Type': 'application/json' }
|
|
332
|
-
})
|
|
333
|
-
} catch {
|
|
334
|
-
return new Response('Source map not found', { status: 404 })
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Ping endpoint for captive portal detection
|
|
339
|
-
if (pathname === core.pingPath && req.method === 'GET') {
|
|
340
|
-
return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
|
|
341
|
-
headers: { 'Content-Type': 'application/json' }
|
|
342
|
-
})
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Not an api-ape route
|
|
346
|
-
return null
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* WebSocket handlers for Bun.serve()
|
|
351
|
-
*/
|
|
352
|
-
const websocket = {
|
|
353
|
-
open(ws) {
|
|
354
|
-
const wrapper = new BunWebSocket(ws)
|
|
355
|
-
clients.set(ws, wrapper)
|
|
356
|
-
const { req } = ws.data || {}
|
|
357
|
-
core.wiringHandler(wrapper, req)
|
|
358
|
-
},
|
|
359
|
-
|
|
360
|
-
message(ws, message) {
|
|
361
|
-
const wrapper = clients.get(ws)
|
|
362
|
-
if (wrapper) {
|
|
363
|
-
wrapper._onMessage(message)
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
close(ws, code, reason) {
|
|
368
|
-
const wrapper = clients.get(ws)
|
|
369
|
-
if (wrapper) {
|
|
370
|
-
wrapper._onClose(code, reason)
|
|
371
|
-
clients.delete(ws)
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
|
|
375
|
-
error(ws, error) {
|
|
376
|
-
const wrapper = clients.get(ws)
|
|
377
|
-
if (wrapper) {
|
|
378
|
-
wrapper._onError(error)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return { fetch, websocket, clients, core }
|
|
384
|
-
}
|
|
363
|
+
module.exports.isBun = isBun;
|
|
385
364
|
|
|
386
365
|
/**
|
|
387
|
-
*
|
|
366
|
+
* Reset singleton state (for testing only)
|
|
367
|
+
* @private
|
|
388
368
|
*/
|
|
389
|
-
|
|
390
|
-
return isBun() && typeof server?.reload === 'function'
|
|
391
|
-
}
|
|
369
|
+
module.exports._resetForTesting = _resetForTesting;
|
|
392
370
|
|
|
393
371
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
372
|
+
* Check if running in Deno runtime
|
|
373
|
+
*
|
|
374
|
+
* @function isDeno
|
|
375
|
+
* @returns {boolean} True if running in Deno
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* const { isDeno } = require('api-ape/server/lib/main')
|
|
379
|
+
*
|
|
380
|
+
* if (isDeno()) {
|
|
381
|
+
* console.log('Running on Deno')
|
|
382
|
+
* }
|
|
396
383
|
*/
|
|
397
|
-
|
|
398
|
-
const { BunWebSocket } = require('./ws/adapters/bun')
|
|
399
|
-
const core = createApeCore(options)
|
|
400
|
-
const clients = new Map()
|
|
401
|
-
|
|
402
|
-
// Check if WebSocket support is enabled on Bun server
|
|
403
|
-
// Bun requires websocket handlers to be defined at Bun.serve() creation
|
|
404
|
-
const hasWebSocketSupport = typeof server.upgrade === 'function'
|
|
405
|
-
|
|
406
|
-
if (!hasWebSocketSupport && options.transport !== 'longpolling') {
|
|
407
|
-
throw new Error(`
|
|
408
|
-
🦍 api-ape: Bun WebSocket support not enabled!
|
|
409
|
-
|
|
410
|
-
To enable WebSocket support in Bun, add a 'websocket' property when creating your server:
|
|
411
|
-
|
|
412
|
-
const server = Bun.serve({
|
|
413
|
-
port: 3000,
|
|
414
|
-
fetch(req) { ... },
|
|
415
|
-
websocket: { message() {} } // <-- Required for api-ape
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
ape(server, { where: 'api' })
|
|
419
|
-
|
|
420
|
-
If you only want HTTP long-polling (no WebSocket), pass:
|
|
421
|
-
ape(server, { where: 'api', transport: 'longpolling' })
|
|
422
|
-
`)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Store original fetch handler
|
|
426
|
-
const originalFetch = server.fetch
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Wrapped fetch handler that handles api-ape routes first
|
|
430
|
-
*/
|
|
431
|
-
function wrappedFetch(req, server) {
|
|
432
|
-
const url = new URL(req.url)
|
|
433
|
-
const pathname = url.pathname
|
|
434
|
-
|
|
435
|
-
// WebSocket upgrade
|
|
436
|
-
if (pathname === core.wsPath) {
|
|
437
|
-
const upgrade = req.headers.get('upgrade')
|
|
438
|
-
if (upgrade?.toLowerCase() === 'websocket') {
|
|
439
|
-
const success = server.upgrade(req, { data: { req } })
|
|
440
|
-
if (success) return undefined
|
|
441
|
-
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Serve client bundle
|
|
446
|
-
if (pathname === core.clientPath) {
|
|
447
|
-
try {
|
|
448
|
-
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
449
|
-
const data = fs.readFileSync(filePath)
|
|
450
|
-
return new Response(data, {
|
|
451
|
-
headers: { 'Content-Type': 'application/javascript' }
|
|
452
|
-
})
|
|
453
|
-
} catch {
|
|
454
|
-
return new Response('Client bundle not found', { status: 500 })
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Serve source map for debugging
|
|
459
|
-
if (pathname === core.clientMapPath) {
|
|
460
|
-
try {
|
|
461
|
-
const filePath = path.join(__dirname, '../../dist/ape.js.map')
|
|
462
|
-
const data = fs.readFileSync(filePath)
|
|
463
|
-
return new Response(data, {
|
|
464
|
-
headers: { 'Content-Type': 'application/json' }
|
|
465
|
-
})
|
|
466
|
-
} catch {
|
|
467
|
-
return new Response('Source map not found', { status: 404 })
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Ping endpoint for captive portal detection
|
|
472
|
-
if (pathname === core.pingPath && req.method === 'GET') {
|
|
473
|
-
return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
|
|
474
|
-
headers: { 'Content-Type': 'application/json' }
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Pass to original fetch handler
|
|
479
|
-
if (originalFetch) {
|
|
480
|
-
return originalFetch(req, server)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return new Response('Not Found', { status: 404 })
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* WebSocket handlers
|
|
488
|
-
*/
|
|
489
|
-
const websocket = {
|
|
490
|
-
open(ws) {
|
|
491
|
-
const wrapper = new BunWebSocket(ws)
|
|
492
|
-
clients.set(ws, wrapper)
|
|
493
|
-
const { req } = ws.data || {}
|
|
494
|
-
core.wiringHandler(wrapper, req)
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
message(ws, message) {
|
|
498
|
-
const wrapper = clients.get(ws)
|
|
499
|
-
if (wrapper) {
|
|
500
|
-
wrapper._onMessage(message)
|
|
501
|
-
}
|
|
502
|
-
},
|
|
503
|
-
|
|
504
|
-
close(ws, code, reason) {
|
|
505
|
-
const wrapper = clients.get(ws)
|
|
506
|
-
if (wrapper) {
|
|
507
|
-
wrapper._onClose(code, reason)
|
|
508
|
-
clients.delete(ws)
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
|
|
512
|
-
error(ws, error) {
|
|
513
|
-
const wrapper = clients.get(ws)
|
|
514
|
-
if (wrapper) {
|
|
515
|
-
wrapper._onError(error)
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Use server.reload() to hook in our handlers
|
|
521
|
-
server.reload({
|
|
522
|
-
fetch: wrappedFetch,
|
|
523
|
-
websocket
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
return { clients, core }
|
|
527
|
-
}
|
|
384
|
+
module.exports.isDeno = isDeno;
|
|
528
385
|
|
|
529
386
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
387
|
+
* Get the detected runtime type
|
|
388
|
+
*
|
|
389
|
+
* @function getRuntime
|
|
390
|
+
* @returns {'deno'|'bun'|'node'|'unknown'} The detected runtime
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* const { getRuntime } = require('api-ape/server/lib/main')
|
|
394
|
+
*
|
|
395
|
+
* console.log(`Running on: ${getRuntime()}`)
|
|
396
|
+
* // Output: 'node', 'bun', 'deno', or 'unknown'
|
|
538
397
|
*/
|
|
539
|
-
module.exports =
|
|
540
|
-
if (created) {
|
|
541
|
-
throw new Error("Api-Ape already started")
|
|
542
|
-
}
|
|
543
|
-
created = true
|
|
544
|
-
|
|
545
|
-
// Bun server - use server.reload() to hook in
|
|
546
|
-
if (isBunServer(server)) {
|
|
547
|
-
return initBunServerWithReload(server, options)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Node.js / Express - server is an http.Server with .on() method
|
|
551
|
-
if (server && typeof server.on === 'function') {
|
|
552
|
-
return initNodeServer(server, options)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
throw new Error('Unsupported server type. Expected http.Server (Node.js) or Bun.serve() server.')
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Export runtime detection utilities
|
|
559
|
-
module.exports.isBun = isBun
|
|
560
|
-
module.exports.isDeno = isDeno
|
|
561
|
-
module.exports.getRuntime = getRuntime
|
|
398
|
+
module.exports.getRuntime = getRuntime;
|