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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketServer class
|
|
3
|
+
* Handles HTTP upgrade requests and creates WebSocket connections
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('events')
|
|
7
|
+
const { generateAcceptKey } = require('./frames')
|
|
8
|
+
const { WebSocket } = require('./socket')
|
|
9
|
+
|
|
10
|
+
class WebSocketServer extends EventEmitter {
|
|
11
|
+
/**
|
|
12
|
+
* Create WebSocket server
|
|
13
|
+
* @param {{ noServer?: boolean }} options
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super()
|
|
17
|
+
this._noServer = options.noServer || false
|
|
18
|
+
this._clients = new Set()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get all connected clients
|
|
23
|
+
* @returns {Set<WebSocket>}
|
|
24
|
+
*/
|
|
25
|
+
get clients() {
|
|
26
|
+
return this._clients
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle HTTP upgrade request
|
|
31
|
+
* @param {http.IncomingMessage} req - HTTP request
|
|
32
|
+
* @param {net.Socket} socket - TCP socket
|
|
33
|
+
* @param {Buffer} head - First packet of upgraded stream
|
|
34
|
+
* @param {function} callback - Called with WebSocket instance
|
|
35
|
+
*/
|
|
36
|
+
handleUpgrade(req, socket, head, callback) {
|
|
37
|
+
// Validate WebSocket upgrade request
|
|
38
|
+
const upgrade = req.headers['upgrade']
|
|
39
|
+
if (!upgrade || upgrade.toLowerCase() !== 'websocket') {
|
|
40
|
+
socket.destroy()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = req.headers['sec-websocket-key']
|
|
45
|
+
if (!key) {
|
|
46
|
+
socket.destroy()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate key is valid base64 (16 bytes = 24 chars base64)
|
|
51
|
+
if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) {
|
|
52
|
+
socket.destroy()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate accept key
|
|
57
|
+
const acceptKey = generateAcceptKey(key)
|
|
58
|
+
|
|
59
|
+
// Build HTTP 101 response
|
|
60
|
+
const headers = [
|
|
61
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
62
|
+
'Upgrade: websocket',
|
|
63
|
+
'Connection: Upgrade',
|
|
64
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
65
|
+
'', '' // Empty line to end headers
|
|
66
|
+
].join('\r\n')
|
|
67
|
+
|
|
68
|
+
// Send handshake response
|
|
69
|
+
socket.write(headers)
|
|
70
|
+
|
|
71
|
+
// If there's buffered data after upgrade, process it
|
|
72
|
+
// The 'head' contains any data received after the upgrade request
|
|
73
|
+
// We'll handle this in the WebSocket's buffer
|
|
74
|
+
|
|
75
|
+
// Create WebSocket wrapper
|
|
76
|
+
const ws = new WebSocket(socket)
|
|
77
|
+
this._clients.add(ws)
|
|
78
|
+
|
|
79
|
+
// Remove from clients on close
|
|
80
|
+
ws.on('close', () => {
|
|
81
|
+
this._clients.delete(ws)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Handle any buffered data
|
|
85
|
+
if (head && head.length > 0) {
|
|
86
|
+
socket.unshift(head)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Call callback with the WebSocket
|
|
90
|
+
if (callback) {
|
|
91
|
+
callback(ws)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Close server and all connections
|
|
97
|
+
*/
|
|
98
|
+
close(callback) {
|
|
99
|
+
for (const client of this._clients) {
|
|
100
|
+
client.close(1001, 'Server shutting down')
|
|
101
|
+
}
|
|
102
|
+
this._clients.clear()
|
|
103
|
+
if (callback) {
|
|
104
|
+
callback()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { WebSocketServer }
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket connection class
|
|
3
|
+
* Wraps a TCP socket and handles WebSocket frame protocol
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('events')
|
|
7
|
+
const {
|
|
8
|
+
OPCODES,
|
|
9
|
+
parseFrame,
|
|
10
|
+
buildFrame,
|
|
11
|
+
buildCloseFrame,
|
|
12
|
+
buildPongFrame,
|
|
13
|
+
parseClosePayload
|
|
14
|
+
} = require('./frames')
|
|
15
|
+
|
|
16
|
+
// WebSocket ready states (matching ws library)
|
|
17
|
+
const READY_STATES = {
|
|
18
|
+
CONNECTING: 0,
|
|
19
|
+
OPEN: 1,
|
|
20
|
+
CLOSING: 2,
|
|
21
|
+
CLOSED: 3
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class WebSocket extends EventEmitter {
|
|
25
|
+
constructor(socket) {
|
|
26
|
+
super()
|
|
27
|
+
this._socket = socket
|
|
28
|
+
this._readyState = READY_STATES.OPEN
|
|
29
|
+
this._buffer = Buffer.alloc(0)
|
|
30
|
+
this._fragments = []
|
|
31
|
+
this._fragmentOpcode = null
|
|
32
|
+
|
|
33
|
+
// Define constants on instance (matching ws library API)
|
|
34
|
+
this.CONNECTING = READY_STATES.CONNECTING
|
|
35
|
+
this.OPEN = READY_STATES.OPEN
|
|
36
|
+
this.CLOSING = READY_STATES.CLOSING
|
|
37
|
+
this.CLOSED = READY_STATES.CLOSED
|
|
38
|
+
|
|
39
|
+
this._setupSocketListeners()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get readyState() {
|
|
43
|
+
return this._readyState
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send data to the client
|
|
48
|
+
* @param {string|Buffer} data - Data to send
|
|
49
|
+
*/
|
|
50
|
+
send(data) {
|
|
51
|
+
if (this._readyState !== READY_STATES.OPEN) {
|
|
52
|
+
throw new Error('WebSocket is not open')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const opcode = Buffer.isBuffer(data) ? OPCODES.BINARY : OPCODES.TEXT
|
|
56
|
+
const frame = buildFrame(data, opcode)
|
|
57
|
+
this._socket.write(frame)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Close the WebSocket connection
|
|
62
|
+
* @param {number} code - Status code
|
|
63
|
+
* @param {string} reason - Close reason
|
|
64
|
+
*/
|
|
65
|
+
close(code = 1000, reason = '') {
|
|
66
|
+
if (this._readyState === READY_STATES.CLOSING ||
|
|
67
|
+
this._readyState === READY_STATES.CLOSED) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this._readyState = READY_STATES.CLOSING
|
|
72
|
+
const frame = buildCloseFrame(code, reason)
|
|
73
|
+
this._socket.write(frame)
|
|
74
|
+
|
|
75
|
+
// Give time for close frame to send, then destroy
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
if (this._readyState !== READY_STATES.CLOSED) {
|
|
78
|
+
this._socket.destroy()
|
|
79
|
+
}
|
|
80
|
+
}, 100)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set up TCP socket event handlers
|
|
85
|
+
*/
|
|
86
|
+
_setupSocketListeners() {
|
|
87
|
+
this._socket.on('data', (data) => {
|
|
88
|
+
this._handleData(data)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
this._socket.on('close', () => {
|
|
92
|
+
this._readyState = READY_STATES.CLOSED
|
|
93
|
+
this.emit('close')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
this._socket.on('error', (err) => {
|
|
97
|
+
this.emit('error', err)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle incoming data, parse frames
|
|
103
|
+
* @param {Buffer} data - Incoming data chunk
|
|
104
|
+
*/
|
|
105
|
+
_handleData(data) {
|
|
106
|
+
// Append to buffer
|
|
107
|
+
this._buffer = Buffer.concat([this._buffer, data])
|
|
108
|
+
|
|
109
|
+
// Process all complete frames in buffer
|
|
110
|
+
while (this._buffer.length > 0) {
|
|
111
|
+
const result = parseFrame(this._buffer)
|
|
112
|
+
if (!result) break // Incomplete frame
|
|
113
|
+
|
|
114
|
+
const { frame, bytesConsumed } = result
|
|
115
|
+
this._buffer = this._buffer.slice(bytesConsumed)
|
|
116
|
+
|
|
117
|
+
this._handleFrame(frame)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle a parsed frame
|
|
123
|
+
* @param {{ fin: boolean, opcode: number, payload: Buffer }} frame
|
|
124
|
+
*/
|
|
125
|
+
_handleFrame(frame) {
|
|
126
|
+
const { fin, opcode, payload } = frame
|
|
127
|
+
|
|
128
|
+
switch (opcode) {
|
|
129
|
+
case OPCODES.CONTINUATION:
|
|
130
|
+
this._handleContinuation(fin, payload)
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
case OPCODES.TEXT:
|
|
134
|
+
case OPCODES.BINARY:
|
|
135
|
+
if (fin) {
|
|
136
|
+
// Complete message
|
|
137
|
+
this._emitMessage(opcode, payload)
|
|
138
|
+
} else {
|
|
139
|
+
// Start of fragmented message
|
|
140
|
+
this._fragments = [payload]
|
|
141
|
+
this._fragmentOpcode = opcode
|
|
142
|
+
}
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
case OPCODES.CLOSE:
|
|
146
|
+
this._handleClose(payload)
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
case OPCODES.PING:
|
|
150
|
+
this._handlePing(payload)
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
case OPCODES.PONG:
|
|
154
|
+
// Pong received, could emit event if needed
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
default:
|
|
158
|
+
// Unknown opcode, close connection
|
|
159
|
+
this.close(1002, 'Unknown opcode')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle continuation frame for fragmented messages
|
|
165
|
+
*/
|
|
166
|
+
_handleContinuation(fin, payload) {
|
|
167
|
+
if (!this._fragmentOpcode) {
|
|
168
|
+
this.close(1002, 'Unexpected continuation frame')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this._fragments.push(payload)
|
|
173
|
+
|
|
174
|
+
if (fin) {
|
|
175
|
+
const completePayload = Buffer.concat(this._fragments)
|
|
176
|
+
this._emitMessage(this._fragmentOpcode, completePayload)
|
|
177
|
+
this._fragments = []
|
|
178
|
+
this._fragmentOpcode = null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Emit message event
|
|
184
|
+
*/
|
|
185
|
+
_emitMessage(opcode, payload) {
|
|
186
|
+
// For text, convert to string; for binary, keep as Buffer
|
|
187
|
+
// But ws library emits Buffer by default, so we match that
|
|
188
|
+
this.emit('message', payload)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle close frame
|
|
193
|
+
*/
|
|
194
|
+
_handleClose(payload) {
|
|
195
|
+
const { code, reason } = parseClosePayload(payload)
|
|
196
|
+
|
|
197
|
+
if (this._readyState === READY_STATES.OPEN) {
|
|
198
|
+
// Send close frame back
|
|
199
|
+
this._readyState = READY_STATES.CLOSING
|
|
200
|
+
const frame = buildCloseFrame(code)
|
|
201
|
+
this._socket.write(frame, () => {
|
|
202
|
+
this._socket.destroy()
|
|
203
|
+
})
|
|
204
|
+
} else {
|
|
205
|
+
this._socket.destroy()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._readyState = READY_STATES.CLOSED
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handle ping frame - respond with pong
|
|
213
|
+
*/
|
|
214
|
+
_handlePing(payload) {
|
|
215
|
+
if (this._readyState === READY_STATES.OPEN) {
|
|
216
|
+
const pong = buildPongFrame(payload)
|
|
217
|
+
this._socket.write(pong)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { WebSocket, READY_STATES }
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Provider
|
|
3
|
+
* Detects runtime capabilities and provides appropriate WebSocket implementation
|
|
4
|
+
*
|
|
5
|
+
* Priority:
|
|
6
|
+
* 1. Deno native WebSocket (Deno.upgradeWebSocket)
|
|
7
|
+
* 2. Bun native WebSocket (Bun.serve websocket)
|
|
8
|
+
* 3. Node.js 24+ stable native WebSocketServer (node:ws module)
|
|
9
|
+
* 4. Custom polyfill (RFC 6455 compliant)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if running in Deno runtime
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function isDeno() {
|
|
17
|
+
return typeof Deno !== 'undefined' && typeof Deno.upgradeWebSocket === 'function'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if running in Bun runtime
|
|
22
|
+
* Uses process.versions.bun which is the recommended detection method
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
function isBun() {
|
|
26
|
+
return typeof process !== 'undefined' && !!process.versions?.bun
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if Node.js version is 24+ stable (not pre-release)
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isNode24Stable() {
|
|
34
|
+
if (typeof process === 'undefined' || !process.versions || !process.versions.node) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Skip if this is actually Bun (Bun also has process.versions.node)
|
|
39
|
+
if (isBun()) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const versionStr = process.versions.node
|
|
44
|
+
const majorVersion = parseInt(versionStr.split('.')[0], 10)
|
|
45
|
+
|
|
46
|
+
// Must be Node 24+
|
|
47
|
+
if (majorVersion < 24) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if this is a stable release (not RC, alpha, beta)
|
|
52
|
+
// Pre-release versions contain hyphens like "24.0.0-rc.1"
|
|
53
|
+
if (versionStr.includes('-')) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the detected runtime type
|
|
62
|
+
* @returns {'deno' | 'bun' | 'node' | 'unknown'}
|
|
63
|
+
*/
|
|
64
|
+
function getRuntime() {
|
|
65
|
+
if (isDeno()) return 'deno'
|
|
66
|
+
if (isBun()) return 'bun'
|
|
67
|
+
if (typeof process !== 'undefined' && process.versions?.node) return 'node'
|
|
68
|
+
return 'unknown'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get WebSocket provider based on runtime capabilities
|
|
73
|
+
* @returns {{ type: string, WebSocketServer: typeof import('./ws').WebSocketServer, runtime: string }}
|
|
74
|
+
*/
|
|
75
|
+
function getWebSocketProvider() {
|
|
76
|
+
const runtime = getRuntime()
|
|
77
|
+
|
|
78
|
+
// 1. Check for Deno runtime
|
|
79
|
+
if (runtime === 'deno') {
|
|
80
|
+
try {
|
|
81
|
+
const { DenoWebSocketServer } = require('./ws/adapters/deno')
|
|
82
|
+
return { type: 'deno-native', WebSocketServer: DenoWebSocketServer, runtime }
|
|
83
|
+
} catch {
|
|
84
|
+
// Adapter not available, fall through to polyfill
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Check for Bun runtime
|
|
89
|
+
if (runtime === 'bun') {
|
|
90
|
+
try {
|
|
91
|
+
const { BunWebSocketServer } = require('./ws/adapters/bun')
|
|
92
|
+
return { type: 'bun-native', WebSocketServer: BunWebSocketServer, runtime }
|
|
93
|
+
} catch {
|
|
94
|
+
// Adapter not available, fall through to polyfill
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. Try Node.js 24+ native WebSocketServer
|
|
99
|
+
if (isNode24Stable()) {
|
|
100
|
+
try {
|
|
101
|
+
const { WebSocketServer } = require('node:ws')
|
|
102
|
+
if (WebSocketServer) {
|
|
103
|
+
return { type: 'node-native', WebSocketServer, runtime }
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// node:ws not available, fall through
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4. Fall back to our polyfill
|
|
111
|
+
const { WebSocketServer } = require('./ws/index')
|
|
112
|
+
return { type: 'polyfill', WebSocketServer, runtime }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cache the provider for consistent usage
|
|
116
|
+
let cachedProvider = null
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get cached WebSocket provider
|
|
120
|
+
* @returns {{ type: string, WebSocketServer: typeof import('./ws').WebSocketServer, runtime: string }}
|
|
121
|
+
*/
|
|
122
|
+
function getCachedProvider() {
|
|
123
|
+
if (!cachedProvider) {
|
|
124
|
+
cachedProvider = getWebSocketProvider()
|
|
125
|
+
}
|
|
126
|
+
return cachedProvider
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
getWebSocketProvider: getCachedProvider,
|
|
131
|
+
getRuntime,
|
|
132
|
+
isDeno,
|
|
133
|
+
isBun,
|
|
134
|
+
isNode24Stable
|
|
135
|
+
}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
const extractRootDomain = require('./extractRootDomain')
|
|
2
2
|
|
|
3
|
+
// Helper to get header that works with both Express and raw Node http
|
|
4
|
+
function getHeader(req, name) {
|
|
5
|
+
// Express-style
|
|
6
|
+
if (typeof req.header === 'function') {
|
|
7
|
+
return req.header(name)
|
|
8
|
+
}
|
|
9
|
+
// Raw Node.js http request
|
|
10
|
+
return req.headers[name.toLowerCase()]
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
module.exports = function (socket, req, onError) {
|
|
4
14
|
onError = onError || console.error
|
|
5
|
-
const origin = extractRootDomain(req
|
|
6
|
-
const host = extractRootDomain(req
|
|
15
|
+
const origin = extractRootDomain(getHeader(req, 'Origin') || "")
|
|
16
|
+
const host = extractRootDomain(getHeader(req, 'Host'))
|
|
7
17
|
if (origin && origin !== host) {
|
|
8
|
-
onError("REJECTING socket from " + req
|
|
9
|
-
socket.destroy
|
|
18
|
+
onError("REJECTING socket from " + getHeader(req, 'Origin') + " miss-match with " + getHeader(req, 'Host'))
|
|
19
|
+
if (socket && socket.destroy) {
|
|
20
|
+
socket.destroy()
|
|
21
|
+
}
|
|
10
22
|
return false
|
|
11
23
|
}
|
|
12
24
|
return true
|
package/server/socket/receive.js
CHANGED
|
@@ -89,9 +89,21 @@ function setValueAtPath(obj, path, value) {
|
|
|
89
89
|
current[parts[parts.length - 1]] = value
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Extract sessionId cookie from request headers
|
|
94
|
+
*/
|
|
95
|
+
function getSessionId(req) {
|
|
96
|
+
const cookies = req?.headers?.cookie || ''
|
|
97
|
+
const match = cookies.match(/(?:^|;\s*)sessionId=([^;]*)/)
|
|
98
|
+
return match ? match[1] : null
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
module.exports = function receiveHandler(ape) {
|
|
93
102
|
const { send, checkReply, events, controllers, sharedValues, hostId, embedValues, fileTransfer } = ape
|
|
94
103
|
|
|
104
|
+
// Extract sessionId from request cookies (set by outer framework session management)
|
|
105
|
+
const sessionId = getSessionId(sharedValues.req)
|
|
106
|
+
|
|
95
107
|
// Build `this` context for controllers
|
|
96
108
|
// Includes: client metadata + api-ape utilities
|
|
97
109
|
const that = {
|
|
@@ -102,7 +114,8 @@ module.exports = function receiveHandler(ape) {
|
|
|
102
114
|
broadcastOthers: (type, data) => broadcast(type, data, hostId), // exclude self
|
|
103
115
|
online,
|
|
104
116
|
getClients,
|
|
105
|
-
hostId
|
|
117
|
+
hostId,
|
|
118
|
+
sessionId // Session ID from cookie (set by outer framework)
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
return async function onReceive(msg) {
|
package/server/socket/send.js
CHANGED
|
@@ -40,7 +40,7 @@ function detectContentType(data) {
|
|
|
40
40
|
* Process data object, replacing binary values with L-tagged hashes
|
|
41
41
|
* Returns { processedData, binaryEntries }
|
|
42
42
|
*/
|
|
43
|
-
function processBinaryData(data, queryId, fileTransfer,
|
|
43
|
+
function processBinaryData(data, queryId, fileTransfer, clientId, path = '') {
|
|
44
44
|
if (data === null || data === undefined) {
|
|
45
45
|
return { processedData: data, binaryEntries: [] }
|
|
46
46
|
}
|
|
@@ -49,7 +49,7 @@ function processBinaryData(data, queryId, fileTransfer, hostId, path = '') {
|
|
|
49
49
|
// This is binary data - register and return hash
|
|
50
50
|
const hash = FileTransferManager.generateHash(queryId, path || 'root')
|
|
51
51
|
const contentType = detectContentType(data)
|
|
52
|
-
fileTransfer.registerDownload(hash, data, contentType,
|
|
52
|
+
fileTransfer.registerDownload(hash, data, contentType, clientId)
|
|
53
53
|
|
|
54
54
|
return {
|
|
55
55
|
processedData: { [`__ape_link__`]: hash },
|
|
@@ -64,7 +64,7 @@ function processBinaryData(data, queryId, fileTransfer, hostId, path = '') {
|
|
|
64
64
|
for (let i = 0; i < data.length; i++) {
|
|
65
65
|
const itemPath = path ? `${path}.${i}` : String(i)
|
|
66
66
|
const { processedData, binaryEntries } = processBinaryData(
|
|
67
|
-
data[i], queryId, fileTransfer,
|
|
67
|
+
data[i], queryId, fileTransfer, clientId, itemPath
|
|
68
68
|
)
|
|
69
69
|
processedArray.push(processedData)
|
|
70
70
|
allBinaryEntries.push(...binaryEntries)
|
|
@@ -80,7 +80,7 @@ function processBinaryData(data, queryId, fileTransfer, hostId, path = '') {
|
|
|
80
80
|
for (const key of Object.keys(data)) {
|
|
81
81
|
const itemPath = path ? `${path}.${key}` : key
|
|
82
82
|
const { processedData, binaryEntries } = processBinaryData(
|
|
83
|
-
data[key], queryId, fileTransfer,
|
|
83
|
+
data[key], queryId, fileTransfer, clientId, itemPath
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
// If this was binary data, mark the key with <!L> tag
|
|
@@ -99,7 +99,7 @@ function processBinaryData(data, queryId, fileTransfer, hostId, path = '') {
|
|
|
99
99
|
return { processedData: data, binaryEntries: [] }
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
module.exports = function sendHandler({ socket, events,
|
|
102
|
+
module.exports = function sendHandler({ socket, events, clientId, fileTransfer }) {
|
|
103
103
|
|
|
104
104
|
return function send(queryId, type, data, err) {
|
|
105
105
|
if (!type && !queryId) {
|
|
@@ -130,7 +130,7 @@ module.exports = function sendHandler({ socket, events, hostId, fileTransfer })
|
|
|
130
130
|
let processedData = data
|
|
131
131
|
if (fileTransfer && data && !err) {
|
|
132
132
|
const { processedData: processed, binaryEntries } = processBinaryData(
|
|
133
|
-
data, queryId || type, fileTransfer,
|
|
133
|
+
data, queryId || type, fileTransfer, clientId
|
|
134
134
|
)
|
|
135
135
|
processedData = processed
|
|
136
136
|
if (binaryEntries.length > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
if(!global.process){//(!!process && typeof process !== 'object'){
|
|
1
|
+
if (!global.process) {//(!!process && typeof process !== 'object'){
|
|
2
2
|
throw new Error("deepRequire need to be run on Node server")
|
|
3
3
|
}
|
|
4
4
|
|
|
@@ -18,7 +18,7 @@ function getFilesFromDir(dir, fileTypes) {
|
|
|
18
18
|
if (fs.statSync(curFile).isFile() && fileTypes.indexOf(path.extname(curFile)) != -1) {
|
|
19
19
|
filesToReturn.push(curFile.replace(dir, ''));
|
|
20
20
|
} else if (fs.statSync(curFile).isDirectory()) {
|
|
21
|
-
|
|
21
|
+
walkDir(curFile);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
};
|
|
@@ -27,19 +27,34 @@ function getFilesFromDir(dir, fileTypes) {
|
|
|
27
27
|
}
|
|
28
28
|
const re = /(?:\.([^.]+))?$/;
|
|
29
29
|
|
|
30
|
-
module.exports = function(dirname,selector){
|
|
30
|
+
module.exports = function (dirname, selector) {
|
|
31
31
|
selector = selector || ["js"]
|
|
32
|
-
|
|
32
|
+
const endpointSources = {} // Track which file defines each endpoint
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
return getFilesFromDir(dirname, selector.map(ext => `.${ext}`)).reduce((packages, file) => {
|
|
35
|
+
|
|
36
|
+
if (file === "/index.js") return packages
|
|
35
37
|
//if(file[0] !== "/") file = "/"+file;
|
|
36
38
|
|
|
37
|
-
const pathParts = file.replace(re.exec(file)[0],"").split("/").slice(1)
|
|
38
|
-
if(pathParts[pathParts.length-1] === "index")
|
|
39
|
-
|
|
39
|
+
const pathParts = file.replace(re.exec(file)[0], "").split("/").slice(1)
|
|
40
|
+
if (pathParts[pathParts.length - 1] === "index")
|
|
41
|
+
pathParts.pop()
|
|
42
|
+
|
|
43
|
+
const endpoint = pathParts.join("/").toLowerCase()
|
|
44
|
+
|
|
45
|
+
// Check for duplicate endpoints
|
|
46
|
+
if (packages[endpoint] !== undefined) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`🦍 Duplicate endpoint detected: "${endpoint}"\n` +
|
|
49
|
+
` - ${endpointSources[endpoint]}\n` +
|
|
50
|
+
` - ${file}\n` +
|
|
51
|
+
` Remove one of these files to fix this conflict.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
endpointSources[endpoint] = file
|
|
56
|
+
packages[endpoint] = require(dirname + `/${file}`)
|
|
42
57
|
return packages;
|
|
43
|
-
},{});
|
|
58
|
+
}, {});
|
|
44
59
|
|
|
45
60
|
}
|