api-ape 2.0.0 → 2.2.2
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 +203 -124
- package/client/README.md +37 -30
- package/client/browser.js +10 -8
- package/client/connectSocket.js +662 -381
- package/client/index.js +171 -0
- package/client/transports/streaming.js +240 -0
- package/dist/ape.js +2 -699
- package/dist/ape.js.map +7 -0
- package/dist/api-ape.min.js +2 -0
- package/dist/api-ape.min.js.map +7 -0
- package/index.d.ts +71 -18
- package/package.json +50 -15
- package/server/README.md +99 -13
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +226 -0
- package/server/lib/main.js +381 -38
- package/server/lib/wiring.js +19 -12
- package/server/lib/ws/adapters/bun.js +225 -0
- package/server/lib/ws/adapters/deno.js +186 -0
- package/server/lib/ws/frames.js +217 -0
- package/server/lib/ws/index.js +15 -0
- package/server/lib/ws/server.js +109 -0
- package/server/lib/ws/socket.js +222 -0
- package/server/lib/wsProvider.js +135 -0
- package/server/security/origin.js +16 -4
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/deepRequire.js +25 -10
- package/server/utils/parseUserAgent.js +286 -0
- package/example/Bun/README.md +0 -74
- package/example/Bun/api/message.ts +0 -11
- package/example/Bun/index.html +0 -76
- package/example/Bun/package.json +0 -9
- package/example/Bun/server.ts +0 -59
- package/example/Bun/styles.css +0 -128
- package/example/ExpressJs/README.md +0 -95
- package/example/ExpressJs/api/message.js +0 -11
- package/example/ExpressJs/backend.js +0 -39
- package/example/ExpressJs/index.html +0 -88
- package/example/ExpressJs/package-lock.json +0 -834
- package/example/ExpressJs/package.json +0 -10
- package/example/ExpressJs/styles.css +0 -128
- package/example/NextJs/.dockerignore +0 -29
- package/example/NextJs/Dockerfile +0 -52
- package/example/NextJs/Dockerfile.dev +0 -27
- package/example/NextJs/README.md +0 -113
- package/example/NextJs/ape/client.js +0 -66
- package/example/NextJs/ape/embed.js +0 -12
- package/example/NextJs/ape/index.js +0 -23
- package/example/NextJs/ape/logic/chat.js +0 -62
- package/example/NextJs/ape/onConnect.js +0 -69
- package/example/NextJs/ape/onDisconnect.js +0 -13
- package/example/NextJs/ape/onError.js +0 -9
- package/example/NextJs/ape/onReceive.js +0 -15
- package/example/NextJs/ape/onSend.js +0 -15
- package/example/NextJs/api/message.js +0 -44
- package/example/NextJs/docker-compose.yml +0 -22
- package/example/NextJs/next-env.d.ts +0 -5
- package/example/NextJs/next.config.js +0 -8
- package/example/NextJs/package-lock.json +0 -6400
- package/example/NextJs/package.json +0 -24
- package/example/NextJs/pages/Info.tsx +0 -153
- package/example/NextJs/pages/_app.tsx +0 -6
- package/example/NextJs/pages/index.tsx +0 -275
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +0 -4
- package/example/NextJs/server.js +0 -36
- package/example/NextJs/styles/Chat.module.css +0 -448
- package/example/NextJs/styles/Home.module.css +0 -129
- package/example/NextJs/styles/globals.css +0 -26
- package/example/NextJs/tsconfig.json +0 -20
- package/example/README.md +0 -117
- package/example/Vite/README.md +0 -68
- package/example/Vite/ape/client.ts +0 -66
- package/example/Vite/ape/onConnect.ts +0 -52
- package/example/Vite/api/message.ts +0 -57
- package/example/Vite/index.html +0 -16
- package/example/Vite/package.json +0 -19
- package/example/Vite/server.ts +0 -62
- package/example/Vite/src/App.vue +0 -170
- package/example/Vite/src/components/Info.vue +0 -352
- package/example/Vite/src/main.ts +0 -5
- package/example/Vite/src/style.css +0 -200
- package/example/Vite/src/vite-env.d.ts +0 -7
- package/example/Vite/vite.config.ts +0 -20
- package/todo.md +0 -85
- package/utils/jss.test.js +0 -261
- package/utils/messageHash.test.js +0 -56
package/server/lib/main.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const loader = require('./loader')
|
|
2
2
|
const wiring = require('./wiring')
|
|
3
|
-
const {
|
|
3
|
+
const { getWebSocketProvider, isBun, isDeno, getRuntime } = require('./wsProvider')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const { getFileTransferManager } = require('./fileTransfer')
|
|
7
|
+
const { createLongPollingHandler } = require('./longPolling')
|
|
7
8
|
const { parse: parseUrl } = require('url')
|
|
8
9
|
|
|
9
10
|
let created = false
|
|
@@ -32,7 +33,7 @@ function matchRoute(pathname, pattern) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* Send JSON response
|
|
36
|
+
* Send JSON response (Node.js style)
|
|
36
37
|
*/
|
|
37
38
|
function sendJson(res, statusCode, data) {
|
|
38
39
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' })
|
|
@@ -40,10 +41,12 @@ function sendJson(res, statusCode, data) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
* Get cookie value from request
|
|
44
|
+
* Get cookie value from request headers
|
|
44
45
|
*/
|
|
45
|
-
function getCookie(
|
|
46
|
-
const cookies =
|
|
46
|
+
function getCookie(headers, name) {
|
|
47
|
+
const cookies = typeof headers.get === 'function'
|
|
48
|
+
? headers.get('cookie')
|
|
49
|
+
: headers.cookie
|
|
47
50
|
if (!cookies) return null
|
|
48
51
|
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
|
49
52
|
return match ? match[1] : null
|
|
@@ -52,42 +55,71 @@ function getCookie(req, name) {
|
|
|
52
55
|
/**
|
|
53
56
|
* Check if request is from localhost
|
|
54
57
|
*/
|
|
55
|
-
function isLocalhost(
|
|
56
|
-
const
|
|
57
|
-
return ['localhost', '127.0.0.1', '[::1]'].includes(
|
|
58
|
+
function isLocalhost(host) {
|
|
59
|
+
const hostname = host?.split(':')[0] || ''
|
|
60
|
+
return ['localhost', '127.0.0.1', '[::1]'].includes(hostname)
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Check if connection is secure (HTTPS)
|
|
62
65
|
*/
|
|
63
66
|
function isSecure(req) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
68
|
-
|
|
69
|
-
if (created) {
|
|
70
|
-
throw new Error("Api-Ape already started")
|
|
67
|
+
if (typeof req.headers?.get === 'function') {
|
|
68
|
+
return req.headers.get('x-forwarded-proto') === 'https'
|
|
71
69
|
}
|
|
72
|
-
|
|
70
|
+
return req.socket?.encrypted || req.headers?.['x-forwarded-proto'] === 'https'
|
|
71
|
+
}
|
|
73
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Create core api-ape handlers (shared between runtimes)
|
|
75
|
+
*/
|
|
76
|
+
function createApeCore({ where, onConnent, fileTransferOptions }) {
|
|
74
77
|
const controllers = loader(where)
|
|
75
78
|
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
79
|
+
const wiringHandler = wiring(controllers, onConnent, fileTransfer)
|
|
80
|
+
const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnent, fileTransfer)
|
|
76
81
|
|
|
77
|
-
// Create WebSocket server attached to the HTTP server
|
|
78
|
-
const wss = new WebSocketServer({ noServer: true })
|
|
79
|
-
|
|
80
|
-
// Handle WebSocket connections
|
|
81
82
|
const wsPath = `/${where}/ape`
|
|
82
|
-
const
|
|
83
|
+
const pollPath = `/${where}/ape/poll`
|
|
84
|
+
const pingPath = `/${where}/ape/ping`
|
|
85
|
+
const clientPath = `/${where}/ape.js`
|
|
86
|
+
const clientMapPath = `/${where}/ape.js.map`
|
|
87
|
+
const downloadPattern = `/${where}/ape/data/:hash`
|
|
88
|
+
const uploadPattern = `/${where}/ape/data/:queryId/:pathHash`
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
return {
|
|
91
|
+
controllers,
|
|
92
|
+
fileTransfer,
|
|
93
|
+
wiringHandler,
|
|
94
|
+
handleStreamGet,
|
|
95
|
+
handleStreamPost,
|
|
96
|
+
wsPath,
|
|
97
|
+
pollPath,
|
|
98
|
+
pingPath,
|
|
99
|
+
clientPath,
|
|
100
|
+
clientMapPath,
|
|
101
|
+
downloadPattern,
|
|
102
|
+
uploadPattern
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Node.js / Express integration
|
|
108
|
+
* Uses server.on('upgrade') and server.on('request')
|
|
109
|
+
*/
|
|
110
|
+
function initNodeServer(server, options) {
|
|
111
|
+
const { WebSocketServer } = getWebSocketProvider()
|
|
112
|
+
const core = createApeCore(options)
|
|
113
|
+
const { where } = options
|
|
114
|
+
|
|
115
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
116
|
+
wss.on('connection', core.wiringHandler)
|
|
85
117
|
|
|
86
118
|
// Handle HTTP upgrade requests for WebSocket
|
|
87
119
|
server.on('upgrade', (req, socket, head) => {
|
|
88
120
|
const { pathname } = parseUrl(req.url)
|
|
89
121
|
|
|
90
|
-
if (pathname === wsPath) {
|
|
122
|
+
if (pathname === core.wsPath) {
|
|
91
123
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
92
124
|
wss.emit('connection', ws, req)
|
|
93
125
|
})
|
|
@@ -104,8 +136,8 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
104
136
|
server.on('request', (req, res) => {
|
|
105
137
|
const { pathname } = parseUrl(req.url)
|
|
106
138
|
|
|
107
|
-
// Serve bundled client
|
|
108
|
-
if (pathname ===
|
|
139
|
+
// Serve bundled client
|
|
140
|
+
if (pathname === core.clientPath) {
|
|
109
141
|
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
110
142
|
fs.readFile(filePath, (err, data) => {
|
|
111
143
|
if (err) {
|
|
@@ -118,21 +150,52 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
118
150
|
return
|
|
119
151
|
}
|
|
120
152
|
|
|
121
|
-
//
|
|
122
|
-
|
|
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)
|
|
123
186
|
if (req.method === 'GET' && downloadMatch) {
|
|
124
187
|
const { hash } = downloadMatch
|
|
125
|
-
const
|
|
188
|
+
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
126
189
|
|
|
127
|
-
if (!
|
|
190
|
+
if (!clientId) {
|
|
128
191
|
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
129
192
|
}
|
|
130
193
|
|
|
131
|
-
if (!isLocalhost(req) && !isSecure(req)) {
|
|
194
|
+
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
132
195
|
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
133
196
|
}
|
|
134
197
|
|
|
135
|
-
const result = fileTransfer.getDownload(hash,
|
|
198
|
+
const result = core.fileTransfer.getDownload(hash, clientId)
|
|
136
199
|
|
|
137
200
|
if (!result) {
|
|
138
201
|
return sendJson(res, 404, { error: 'Download not found or unauthorized' })
|
|
@@ -146,17 +209,17 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
146
209
|
return
|
|
147
210
|
}
|
|
148
211
|
|
|
149
|
-
// File upload
|
|
150
|
-
const uploadMatch = matchRoute(pathname,
|
|
212
|
+
// File upload
|
|
213
|
+
const uploadMatch = matchRoute(pathname, core.uploadPattern)
|
|
151
214
|
if (req.method === 'PUT' && uploadMatch) {
|
|
152
215
|
const { queryId, pathHash } = uploadMatch
|
|
153
|
-
const
|
|
216
|
+
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
154
217
|
|
|
155
|
-
if (!
|
|
218
|
+
if (!clientId) {
|
|
156
219
|
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
157
220
|
}
|
|
158
221
|
|
|
159
|
-
if (!isLocalhost(req) && !isSecure(req)) {
|
|
222
|
+
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
160
223
|
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
161
224
|
}
|
|
162
225
|
|
|
@@ -164,7 +227,7 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
164
227
|
req.on('data', chunk => chunks.push(chunk))
|
|
165
228
|
req.on('end', () => {
|
|
166
229
|
const data = Buffer.concat(chunks)
|
|
167
|
-
const success = fileTransfer.receiveUpload(queryId, pathHash, data,
|
|
230
|
+
const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
|
|
168
231
|
|
|
169
232
|
if (success) {
|
|
170
233
|
sendJson(res, 200, { success: true })
|
|
@@ -183,4 +246,284 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
183
246
|
listener.call(server, req, res)
|
|
184
247
|
}
|
|
185
248
|
})
|
|
186
|
-
|
|
249
|
+
|
|
250
|
+
return { wss, core }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Bun integration
|
|
255
|
+
* Returns fetch and websocket handlers to spread into Bun.serve()
|
|
256
|
+
*/
|
|
257
|
+
function initBunServer(options) {
|
|
258
|
+
const { BunWebSocket } = require('./ws/adapters/bun')
|
|
259
|
+
const core = createApeCore(options)
|
|
260
|
+
const clients = new Map()
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Fetch handler for Bun.serve()
|
|
264
|
+
* Handles all api-ape routes, returns null for non-ape routes
|
|
265
|
+
*/
|
|
266
|
+
function fetch(req, server) {
|
|
267
|
+
const url = new URL(req.url)
|
|
268
|
+
const pathname = url.pathname
|
|
269
|
+
|
|
270
|
+
// WebSocket upgrade
|
|
271
|
+
if (pathname === core.wsPath) {
|
|
272
|
+
const upgrade = req.headers.get('upgrade')
|
|
273
|
+
if (upgrade?.toLowerCase() === 'websocket') {
|
|
274
|
+
const success = server.upgrade(req, { data: { req } })
|
|
275
|
+
if (success) return undefined
|
|
276
|
+
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Serve client bundle
|
|
281
|
+
if (pathname === core.clientPath) {
|
|
282
|
+
try {
|
|
283
|
+
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
284
|
+
const data = fs.readFileSync(filePath)
|
|
285
|
+
return new Response(data, {
|
|
286
|
+
headers: { 'Content-Type': 'application/javascript' }
|
|
287
|
+
})
|
|
288
|
+
} catch {
|
|
289
|
+
return new Response('Client bundle not found', { status: 500 })
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Serve source map for debugging
|
|
294
|
+
if (pathname === core.clientMapPath) {
|
|
295
|
+
try {
|
|
296
|
+
const filePath = path.join(__dirname, '../../dist/ape.js.map')
|
|
297
|
+
const data = fs.readFileSync(filePath)
|
|
298
|
+
return new Response(data, {
|
|
299
|
+
headers: { 'Content-Type': 'application/json' }
|
|
300
|
+
})
|
|
301
|
+
} catch {
|
|
302
|
+
return new Response('Source map not found', { status: 404 })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Ping endpoint for captive portal detection
|
|
307
|
+
if (pathname === core.pingPath && req.method === 'GET') {
|
|
308
|
+
return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
|
|
309
|
+
headers: { 'Content-Type': 'application/json' }
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Not an api-ape route
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* WebSocket handlers for Bun.serve()
|
|
319
|
+
*/
|
|
320
|
+
const websocket = {
|
|
321
|
+
open(ws) {
|
|
322
|
+
const wrapper = new BunWebSocket(ws)
|
|
323
|
+
clients.set(ws, wrapper)
|
|
324
|
+
const { req } = ws.data || {}
|
|
325
|
+
core.wiringHandler(wrapper, req)
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
message(ws, message) {
|
|
329
|
+
const wrapper = clients.get(ws)
|
|
330
|
+
if (wrapper) {
|
|
331
|
+
wrapper._onMessage(message)
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
close(ws, code, reason) {
|
|
336
|
+
const wrapper = clients.get(ws)
|
|
337
|
+
if (wrapper) {
|
|
338
|
+
wrapper._onClose(code, reason)
|
|
339
|
+
clients.delete(ws)
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
error(ws, error) {
|
|
344
|
+
const wrapper = clients.get(ws)
|
|
345
|
+
if (wrapper) {
|
|
346
|
+
wrapper._onError(error)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { fetch, websocket, clients, core }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check if server is a Bun server (has reload method and Bun globals)
|
|
356
|
+
*/
|
|
357
|
+
function isBunServer(server) {
|
|
358
|
+
return isBun() && typeof server?.reload === 'function'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Initialize Bun server using server.reload() to hook in
|
|
363
|
+
* This allows same signature: ape(server, { where: 'api' })
|
|
364
|
+
*/
|
|
365
|
+
function initBunServerWithReload(server, options) {
|
|
366
|
+
const { BunWebSocket } = require('./ws/adapters/bun')
|
|
367
|
+
const core = createApeCore(options)
|
|
368
|
+
const clients = new Map()
|
|
369
|
+
|
|
370
|
+
// Check if WebSocket support is enabled on Bun server
|
|
371
|
+
// Bun requires websocket handlers to be defined at Bun.serve() creation
|
|
372
|
+
const hasWebSocketSupport = typeof server.upgrade === 'function'
|
|
373
|
+
|
|
374
|
+
if (!hasWebSocketSupport && options.transport !== 'longpolling') {
|
|
375
|
+
throw new Error(`
|
|
376
|
+
🦍 api-ape: Bun WebSocket support not enabled!
|
|
377
|
+
|
|
378
|
+
To enable WebSocket support in Bun, add a 'websocket' property when creating your server:
|
|
379
|
+
|
|
380
|
+
const server = Bun.serve({
|
|
381
|
+
port: 3000,
|
|
382
|
+
fetch(req) { ... },
|
|
383
|
+
websocket: { message() {} } // <-- Required for api-ape
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
ape(server, { where: 'api' })
|
|
387
|
+
|
|
388
|
+
If you only want HTTP long-polling (no WebSocket), pass:
|
|
389
|
+
ape(server, { where: 'api', transport: 'longpolling' })
|
|
390
|
+
`)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Store original fetch handler
|
|
394
|
+
const originalFetch = server.fetch
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Wrapped fetch handler that handles api-ape routes first
|
|
398
|
+
*/
|
|
399
|
+
function wrappedFetch(req, server) {
|
|
400
|
+
const url = new URL(req.url)
|
|
401
|
+
const pathname = url.pathname
|
|
402
|
+
|
|
403
|
+
// WebSocket upgrade
|
|
404
|
+
if (pathname === core.wsPath) {
|
|
405
|
+
const upgrade = req.headers.get('upgrade')
|
|
406
|
+
if (upgrade?.toLowerCase() === 'websocket') {
|
|
407
|
+
const success = server.upgrade(req, { data: { req } })
|
|
408
|
+
if (success) return undefined
|
|
409
|
+
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Serve client bundle
|
|
414
|
+
if (pathname === core.clientPath) {
|
|
415
|
+
try {
|
|
416
|
+
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
417
|
+
const data = fs.readFileSync(filePath)
|
|
418
|
+
return new Response(data, {
|
|
419
|
+
headers: { 'Content-Type': 'application/javascript' }
|
|
420
|
+
})
|
|
421
|
+
} catch {
|
|
422
|
+
return new Response('Client bundle not found', { status: 500 })
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Serve source map for debugging
|
|
427
|
+
if (pathname === core.clientMapPath) {
|
|
428
|
+
try {
|
|
429
|
+
const filePath = path.join(__dirname, '../../dist/ape.js.map')
|
|
430
|
+
const data = fs.readFileSync(filePath)
|
|
431
|
+
return new Response(data, {
|
|
432
|
+
headers: { 'Content-Type': 'application/json' }
|
|
433
|
+
})
|
|
434
|
+
} catch {
|
|
435
|
+
return new Response('Source map not found', { status: 404 })
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Ping endpoint for captive portal detection
|
|
440
|
+
if (pathname === core.pingPath && req.method === 'GET') {
|
|
441
|
+
return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
|
|
442
|
+
headers: { 'Content-Type': 'application/json' }
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Pass to original fetch handler
|
|
447
|
+
if (originalFetch) {
|
|
448
|
+
return originalFetch(req, server)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return new Response('Not Found', { status: 404 })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* WebSocket handlers
|
|
456
|
+
*/
|
|
457
|
+
const websocket = {
|
|
458
|
+
open(ws) {
|
|
459
|
+
const wrapper = new BunWebSocket(ws)
|
|
460
|
+
clients.set(ws, wrapper)
|
|
461
|
+
const { req } = ws.data || {}
|
|
462
|
+
core.wiringHandler(wrapper, req)
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
message(ws, message) {
|
|
466
|
+
const wrapper = clients.get(ws)
|
|
467
|
+
if (wrapper) {
|
|
468
|
+
wrapper._onMessage(message)
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
close(ws, code, reason) {
|
|
473
|
+
const wrapper = clients.get(ws)
|
|
474
|
+
if (wrapper) {
|
|
475
|
+
wrapper._onClose(code, reason)
|
|
476
|
+
clients.delete(ws)
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
error(ws, error) {
|
|
481
|
+
const wrapper = clients.get(ws)
|
|
482
|
+
if (wrapper) {
|
|
483
|
+
wrapper._onError(error)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Use server.reload() to hook in our handlers
|
|
489
|
+
server.reload({
|
|
490
|
+
fetch: wrappedFetch,
|
|
491
|
+
websocket
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
return { clients, core }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Main api-ape entry point
|
|
499
|
+
* Unified signature for all runtimes:
|
|
500
|
+
* ape(server, { where: 'api' })
|
|
501
|
+
*
|
|
502
|
+
* Works with:
|
|
503
|
+
* - Node.js http.Server
|
|
504
|
+
* - Express server
|
|
505
|
+
* - Bun.serve() server
|
|
506
|
+
*/
|
|
507
|
+
module.exports = function (server, options) {
|
|
508
|
+
if (created) {
|
|
509
|
+
throw new Error("Api-Ape already started")
|
|
510
|
+
}
|
|
511
|
+
created = true
|
|
512
|
+
|
|
513
|
+
// Bun server - use server.reload() to hook in
|
|
514
|
+
if (isBunServer(server)) {
|
|
515
|
+
return initBunServerWithReload(server, options)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Node.js / Express - server is an http.Server with .on() method
|
|
519
|
+
if (server && typeof server.on === 'function') {
|
|
520
|
+
return initNodeServer(server, options)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
throw new Error('Unsupported server type. Expected http.Server (Node.js) or Bun.serve() server.')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Export runtime detection utilities
|
|
527
|
+
module.exports.isBun = isBun
|
|
528
|
+
module.exports.isDeno = isDeno
|
|
529
|
+
module.exports.getRuntime = getRuntime
|
package/server/lib/wiring.js
CHANGED
|
@@ -3,11 +3,9 @@ const socketOpen = require('../socket/open')
|
|
|
3
3
|
const socketReceive = require('../socket/receive')
|
|
4
4
|
const socketSend = require('../socket/send')
|
|
5
5
|
const makeid = require('../utils/genId')
|
|
6
|
-
const
|
|
6
|
+
const parseUserAgent = require('../utils/parseUserAgent');
|
|
7
7
|
const { addClient, removeClient } = require('./broadcast')
|
|
8
8
|
|
|
9
|
-
const parser = new UAParser();
|
|
10
|
-
|
|
11
9
|
// connent, beforeSend, beforeReceive, error, afterSend, afterReceive, disconnent
|
|
12
10
|
|
|
13
11
|
|
|
@@ -40,13 +38,22 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
40
38
|
}
|
|
41
39
|
} // END sentBufferFn
|
|
42
40
|
|
|
43
|
-
const
|
|
44
|
-
const agent =
|
|
41
|
+
const clientId = makeid(20)
|
|
42
|
+
const agent = parseUserAgent(req.headers['user-agent'])
|
|
45
43
|
const sharedValues = {
|
|
46
44
|
socket, req, agent, send: (type, data, err) => sentBufferFn(false, type, data, err)
|
|
47
45
|
}
|
|
48
|
-
sharedValues.send.toString = () =>
|
|
46
|
+
sharedValues.send.toString = () => clientId
|
|
47
|
+
|
|
48
|
+
// Track this client for broadcast BEFORE calling onConnent
|
|
49
|
+
// This ensures ape.online() returns the correct count when sending init
|
|
50
|
+
const clientInfo = { clientId, send: null, embed: null }
|
|
51
|
+
addClient(clientInfo)
|
|
49
52
|
|
|
53
|
+
// Remove client on disconnect (set up early, will work once send is assigned)
|
|
54
|
+
socket.on('close', () => {
|
|
55
|
+
removeClient(clientInfo)
|
|
56
|
+
})
|
|
50
57
|
|
|
51
58
|
let result = onConnent(socket, req, sharedValues.send)
|
|
52
59
|
if (!result || !result.then) {
|
|
@@ -57,6 +64,7 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
57
64
|
const isOk = socketOpen(socket, req, onError)
|
|
58
65
|
|
|
59
66
|
if (!isOk) {
|
|
67
|
+
removeClient(clientInfo) // Clean up if connection fails
|
|
60
68
|
return;
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -65,7 +73,7 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
65
73
|
const ape = {
|
|
66
74
|
socket,
|
|
67
75
|
req,
|
|
68
|
-
|
|
76
|
+
clientId,
|
|
69
77
|
checkReply,
|
|
70
78
|
events: { onReceive, onSend, onError, onDisconnent },
|
|
71
79
|
controllers,
|
|
@@ -76,13 +84,12 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
76
84
|
send = socketSend(ape)
|
|
77
85
|
ape.send = send
|
|
78
86
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
// Update clientInfo with real send function and embed
|
|
88
|
+
clientInfo.send = send
|
|
89
|
+
clientInfo.embed = embed
|
|
82
90
|
|
|
83
|
-
//
|
|
91
|
+
// Call onDisconnent when socket closes
|
|
84
92
|
socket.on('close', () => {
|
|
85
|
-
removeClient(clientInfo)
|
|
86
93
|
onDisconnent()
|
|
87
94
|
})
|
|
88
95
|
|