api-ape 2.1.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 +184 -195
- package/client/README.md +37 -30
- package/client/browser.js +4 -14
- package/client/connectSocket.js +167 -42
- package/client/index.js +171 -0
- package/client/transports/streaming.js +3 -16
- package/dist/ape.js +2 -1049
- 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 +67 -23
- package/package.json +27 -8
- package/server/README.md +52 -11
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +28 -23
- package/server/lib/main.js +372 -46
- 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/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- 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,6 +1,6 @@
|
|
|
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')
|
|
@@ -33,7 +33,7 @@ function matchRoute(pathname, pattern) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Send JSON response
|
|
36
|
+
* Send JSON response (Node.js style)
|
|
37
37
|
*/
|
|
38
38
|
function sendJson(res, statusCode, data) {
|
|
39
39
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' })
|
|
@@ -41,10 +41,12 @@ function sendJson(res, statusCode, data) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Get cookie value from request
|
|
44
|
+
* Get cookie value from request headers
|
|
45
45
|
*/
|
|
46
|
-
function getCookie(
|
|
47
|
-
const cookies =
|
|
46
|
+
function getCookie(headers, name) {
|
|
47
|
+
const cookies = typeof headers.get === 'function'
|
|
48
|
+
? headers.get('cookie')
|
|
49
|
+
: headers.cookie
|
|
48
50
|
if (!cookies) return null
|
|
49
51
|
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
|
50
52
|
return match ? match[1] : null
|
|
@@ -53,46 +55,71 @@ function getCookie(req, name) {
|
|
|
53
55
|
/**
|
|
54
56
|
* Check if request is from localhost
|
|
55
57
|
*/
|
|
56
|
-
function isLocalhost(
|
|
57
|
-
const
|
|
58
|
-
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)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Check if connection is secure (HTTPS)
|
|
63
65
|
*/
|
|
64
66
|
function isSecure(req) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
69
|
-
|
|
70
|
-
if (created) {
|
|
71
|
-
throw new Error("Api-Ape already started")
|
|
67
|
+
if (typeof req.headers?.get === 'function') {
|
|
68
|
+
return req.headers.get('x-forwarded-proto') === 'https'
|
|
72
69
|
}
|
|
73
|
-
|
|
70
|
+
return req.socket?.encrypted || req.headers?.['x-forwarded-proto'] === 'https'
|
|
71
|
+
}
|
|
74
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Create core api-ape handlers (shared between runtimes)
|
|
75
|
+
*/
|
|
76
|
+
function createApeCore({ where, onConnent, fileTransferOptions }) {
|
|
75
77
|
const controllers = loader(where)
|
|
76
78
|
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
79
|
+
const wiringHandler = wiring(controllers, onConnent, fileTransfer)
|
|
80
|
+
const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnent, fileTransfer)
|
|
77
81
|
|
|
78
|
-
// Create WebSocket server attached to the HTTP server
|
|
79
|
-
const wss = new WebSocketServer({ noServer: true })
|
|
80
|
-
|
|
81
|
-
// Handle WebSocket connections
|
|
82
82
|
const wsPath = `/${where}/ape`
|
|
83
83
|
const pollPath = `/${where}/ape/poll`
|
|
84
|
-
const
|
|
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`
|
|
89
|
+
|
|
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
|
+
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
88
114
|
|
|
89
|
-
wss
|
|
115
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
116
|
+
wss.on('connection', core.wiringHandler)
|
|
90
117
|
|
|
91
118
|
// Handle HTTP upgrade requests for WebSocket
|
|
92
119
|
server.on('upgrade', (req, socket, head) => {
|
|
93
120
|
const { pathname } = parseUrl(req.url)
|
|
94
121
|
|
|
95
|
-
if (pathname === wsPath) {
|
|
122
|
+
if (pathname === core.wsPath) {
|
|
96
123
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
97
124
|
wss.emit('connection', ws, req)
|
|
98
125
|
})
|
|
@@ -109,8 +136,8 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
109
136
|
server.on('request', (req, res) => {
|
|
110
137
|
const { pathname } = parseUrl(req.url)
|
|
111
138
|
|
|
112
|
-
// Serve bundled client
|
|
113
|
-
if (pathname ===
|
|
139
|
+
// Serve bundled client
|
|
140
|
+
if (pathname === core.clientPath) {
|
|
114
141
|
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
115
142
|
fs.readFile(filePath, (err, data) => {
|
|
116
143
|
if (err) {
|
|
@@ -123,33 +150,52 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
123
150
|
return
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
//
|
|
127
|
-
if (pathname ===
|
|
128
|
-
|
|
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
|
+
})
|
|
129
164
|
return
|
|
130
165
|
}
|
|
131
166
|
|
|
132
|
-
//
|
|
133
|
-
if (pathname ===
|
|
134
|
-
|
|
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)
|
|
135
175
|
return
|
|
136
176
|
}
|
|
137
177
|
|
|
138
|
-
//
|
|
139
|
-
|
|
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)
|
|
140
186
|
if (req.method === 'GET' && downloadMatch) {
|
|
141
187
|
const { hash } = downloadMatch
|
|
142
|
-
const
|
|
188
|
+
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
143
189
|
|
|
144
|
-
if (!
|
|
190
|
+
if (!clientId) {
|
|
145
191
|
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
146
192
|
}
|
|
147
193
|
|
|
148
|
-
if (!isLocalhost(req) && !isSecure(req)) {
|
|
194
|
+
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
149
195
|
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
150
196
|
}
|
|
151
197
|
|
|
152
|
-
const result = fileTransfer.getDownload(hash,
|
|
198
|
+
const result = core.fileTransfer.getDownload(hash, clientId)
|
|
153
199
|
|
|
154
200
|
if (!result) {
|
|
155
201
|
return sendJson(res, 404, { error: 'Download not found or unauthorized' })
|
|
@@ -163,17 +209,17 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
163
209
|
return
|
|
164
210
|
}
|
|
165
211
|
|
|
166
|
-
// File upload
|
|
167
|
-
const uploadMatch = matchRoute(pathname,
|
|
212
|
+
// File upload
|
|
213
|
+
const uploadMatch = matchRoute(pathname, core.uploadPattern)
|
|
168
214
|
if (req.method === 'PUT' && uploadMatch) {
|
|
169
215
|
const { queryId, pathHash } = uploadMatch
|
|
170
|
-
const
|
|
216
|
+
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
171
217
|
|
|
172
|
-
if (!
|
|
218
|
+
if (!clientId) {
|
|
173
219
|
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
174
220
|
}
|
|
175
221
|
|
|
176
|
-
if (!isLocalhost(req) && !isSecure(req)) {
|
|
222
|
+
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
177
223
|
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
178
224
|
}
|
|
179
225
|
|
|
@@ -181,7 +227,7 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
181
227
|
req.on('data', chunk => chunks.push(chunk))
|
|
182
228
|
req.on('end', () => {
|
|
183
229
|
const data = Buffer.concat(chunks)
|
|
184
|
-
const success = fileTransfer.receiveUpload(queryId, pathHash, data,
|
|
230
|
+
const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
|
|
185
231
|
|
|
186
232
|
if (success) {
|
|
187
233
|
sendJson(res, 200, { success: true })
|
|
@@ -200,4 +246,284 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
|
200
246
|
listener.call(server, req, res)
|
|
201
247
|
}
|
|
202
248
|
})
|
|
203
|
-
|
|
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
|
|