api-ape 2.1.0 → 2.2.3
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 +28 -9
- 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/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# 🦍 api-ape
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/api-ape)
|
|
4
|
-
[](https://github.com/codemeasandwich/api-ape/blob/main/LICENSE)
|
|
5
4
|
[](https://github.com/codemeasandwich/api-ape/issues)
|
|
5
|
+
[](#zero-dependencies)
|
|
6
|
+
[](#csrf-protection)
|
|
7
|
+
[](https://bundlephobia.com/package/api-ape)
|
|
8
|
+
[](#jjs-encoding)
|
|
9
|
+
[](https://github.com/codemeasandwich/api-ape/blob/main/LICENSE)
|
|
6
10
|
|
|
7
11
|
**Remote Procedure Events (RPE)** — A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods. Get real-time broadcasts with zero setup.
|
|
8
12
|
|
|
@@ -69,16 +73,36 @@ Include the bundled client and start calling:
|
|
|
69
73
|
<script src="/api/ape.js"></script>
|
|
70
74
|
<script>
|
|
71
75
|
// Call server functions like local methods
|
|
72
|
-
const result = await
|
|
76
|
+
const result = await api.hello('World')
|
|
73
77
|
console.log(result) // "Hello, World!"
|
|
74
78
|
|
|
75
79
|
// Listen for broadcasts
|
|
76
|
-
|
|
80
|
+
api.on('message', ({ data }) => {
|
|
77
81
|
console.log('New message:', data)
|
|
78
82
|
})
|
|
79
83
|
</script>
|
|
80
84
|
```
|
|
81
85
|
|
|
86
|
+
### Client (React, Vue, etc.)
|
|
87
|
+
|
|
88
|
+
With bundlers, use the unified import — no async setup needed:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import api from 'api-ape'
|
|
92
|
+
|
|
93
|
+
// Just use it! Calls are buffered until connected.
|
|
94
|
+
const result = await api.hello('World')
|
|
95
|
+
|
|
96
|
+
// Listen for broadcasts
|
|
97
|
+
api.on('message', ({ data }) => console.log(data))
|
|
98
|
+
|
|
99
|
+
// Track connection state
|
|
100
|
+
api.onConnectionChange((state) => {
|
|
101
|
+
console.log('Connection:', state)
|
|
102
|
+
// 'offline' | 'walled' | 'disconnected' | 'connecting' | 'connected'
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
82
106
|
**That's it!** Your server function is now callable from the browser.
|
|
83
107
|
|
|
84
108
|
---
|
|
@@ -87,7 +111,7 @@ Include the bundled client and start calling:
|
|
|
87
111
|
|
|
88
112
|
* **Auto-wiring** — Drop JS files in a folder, they become API endpoints automatically
|
|
89
113
|
* **Real-time broadcasts** — Built-in `broadcast()` and `broadcastOthers()` methods for pushing to clients
|
|
90
|
-
* **Promise-based calls** — Chainable paths like `
|
|
114
|
+
* **Promise-based calls** — Chainable paths like `api.users.list()` map to `api/users/list.js`
|
|
91
115
|
* **Automatic reconnection** — Client auto-reconnects on disconnect with exponential backoff
|
|
92
116
|
* **HTTP streaming fallback** — Automatically falls back to long polling when WebSockets are blocked
|
|
93
117
|
* **JJS Encoding** — Extended JSON supporting Date, RegExp, Error, Set, Map, undefined, and circular refs
|
|
@@ -118,88 +142,74 @@ Inside controller functions, `this` provides:
|
|
|
118
142
|
| `this.broadcast(type, data)` | Send to **ALL** connected clients |
|
|
119
143
|
| `this.broadcastOthers(type, data)` | Send to all **EXCEPT** the caller |
|
|
120
144
|
| `this.online()` | Get count of connected clients |
|
|
121
|
-
| `this.getClients()` | Get array of connected
|
|
122
|
-
| `this.
|
|
145
|
+
| `this.getClients()` | Get array of connected clientIds |
|
|
146
|
+
| `this.clientId` | Unique ID of the calling client (generated by api-ape) |
|
|
147
|
+
| `this.sessionId` | Session ID from cookie (set by outer framework, may be `null`) |
|
|
123
148
|
| `this.req` | Original HTTP request |
|
|
124
149
|
| `this.socket` | WebSocket instance |
|
|
125
150
|
| `this.agent` | Parsed user-agent (browser, OS, device) |
|
|
126
151
|
|
|
127
152
|
### Client
|
|
128
153
|
|
|
129
|
-
#### `
|
|
154
|
+
#### `api.<path>.<method>(...args)`
|
|
130
155
|
|
|
131
156
|
Call a server function. Returns a Promise.
|
|
132
157
|
|
|
133
158
|
```js
|
|
134
159
|
// Calls api/users/list.js
|
|
135
|
-
const users = await
|
|
160
|
+
const users = await api.users.list()
|
|
136
161
|
|
|
137
162
|
// Calls api/users/create.js with data
|
|
138
|
-
const user = await
|
|
163
|
+
const user = await api.users.create({ name: 'Alice' })
|
|
139
164
|
|
|
140
165
|
// Nested paths work too
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
await
|
|
166
|
+
// api.admin.users -> api/admin/users.js
|
|
167
|
+
// api.admin.users.delete -> api/admin/users/delete.js
|
|
168
|
+
await api.admin.users.delete(userId)
|
|
144
169
|
```
|
|
145
170
|
|
|
146
|
-
#### `
|
|
171
|
+
#### `api.on(type, handler)`
|
|
147
172
|
|
|
148
173
|
Listen for server broadcasts.
|
|
149
174
|
|
|
150
175
|
```js
|
|
151
|
-
|
|
176
|
+
api.on('notification', ({ data, err, type }) => {
|
|
152
177
|
console.log('Received:', data)
|
|
153
178
|
})
|
|
154
179
|
```
|
|
155
180
|
|
|
156
|
-
#### `
|
|
157
|
-
|
|
158
|
-
Configure client connection options.
|
|
159
|
-
|
|
160
|
-
```js
|
|
161
|
-
// Configure transport mode
|
|
162
|
-
ape.configure({ transport: 'auto' }) // Auto-detect (default)
|
|
163
|
-
ape.configure({ transport: 'websocket' }) // Force WebSocket only
|
|
164
|
-
ape.configure({ transport: 'polling' }) // Force HTTP streaming
|
|
165
|
-
|
|
166
|
-
// Configure connection details
|
|
167
|
-
ape.configure({
|
|
168
|
-
port: 9010,
|
|
169
|
-
host: 'example.com',
|
|
170
|
-
transport: 'auto'
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
| Option | Type | Description |
|
|
175
|
-
|--------|------|-------------|
|
|
176
|
-
| `port` | `number` | Server port (default: auto-detect) |
|
|
177
|
-
| `host` | `string` | Server hostname (default: auto-detect) |
|
|
178
|
-
| `transport` | `string` | Transport mode: `'auto'`, `'websocket'`, or `'polling'` |
|
|
179
|
-
|
|
180
|
-
#### `ape.getTransport()`
|
|
181
|
+
#### `api.getTransport()`
|
|
181
182
|
|
|
182
183
|
Get the currently active transport type.
|
|
183
184
|
|
|
184
185
|
```js
|
|
185
|
-
const transport =
|
|
186
|
+
const transport = api.getTransport()
|
|
186
187
|
console.log(transport) // 'websocket' | 'polling' | null
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
#### `
|
|
190
|
+
#### `api.onConnectionChange(handler)`
|
|
190
191
|
|
|
191
192
|
Listen for connection state changes.
|
|
192
193
|
|
|
193
194
|
```js
|
|
194
|
-
const unsubscribe =
|
|
195
|
+
const unsubscribe = api.onConnectionChange((state) => {
|
|
195
196
|
console.log('Connection state:', state)
|
|
196
|
-
// 'disconnected' | 'connecting' | 'connected'
|
|
197
197
|
})
|
|
198
198
|
|
|
199
199
|
// Later: stop listening
|
|
200
200
|
unsubscribe()
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
**Connection States:**
|
|
204
|
+
|
|
205
|
+
| State | Description |
|
|
206
|
+
|-------|-------------|
|
|
207
|
+
| `offline` | Browser reports no network (`navigator.onLine = false`) |
|
|
208
|
+
| `walled` | Captive portal detected (can't reach server) |
|
|
209
|
+
| `disconnected` | Had connection, lost it |
|
|
210
|
+
| `connecting` | Actively connecting to server |
|
|
211
|
+
| `connected` | Connected and ready |
|
|
212
|
+
|
|
203
213
|
---
|
|
204
214
|
|
|
205
215
|
## Configuration
|
|
@@ -208,8 +218,7 @@ unsubscribe()
|
|
|
208
218
|
|
|
209
219
|
```js
|
|
210
220
|
ape(server, {
|
|
211
|
-
where: 'api'
|
|
212
|
-
onConnent: undefined // Lifecycle hook (optional)
|
|
221
|
+
where: 'api' // Controller directory
|
|
213
222
|
})
|
|
214
223
|
```
|
|
215
224
|
|
|
@@ -220,12 +229,12 @@ Customize behavior per connection:
|
|
|
220
229
|
```js
|
|
221
230
|
ape(server, {
|
|
222
231
|
where: 'api',
|
|
223
|
-
onConnent(socket, req,
|
|
232
|
+
onConnent(socket, req, clientId) {
|
|
224
233
|
return {
|
|
225
234
|
// Embed values into `this` for all controllers
|
|
226
235
|
embed: {
|
|
227
236
|
userId: req.session?.userId,
|
|
228
|
-
|
|
237
|
+
id: String(clientId)
|
|
229
238
|
},
|
|
230
239
|
|
|
231
240
|
// Before/after hooks
|
|
@@ -316,7 +325,7 @@ module.exports = async function(id) {
|
|
|
316
325
|
|
|
317
326
|
```js
|
|
318
327
|
try {
|
|
319
|
-
const result = await
|
|
328
|
+
const result = await api.data.get(id)
|
|
320
329
|
console.log(result)
|
|
321
330
|
} catch (err) {
|
|
322
331
|
console.error('Server error:', err)
|
|
@@ -325,82 +334,25 @@ try {
|
|
|
325
334
|
|
|
326
335
|
---
|
|
327
336
|
|
|
328
|
-
## JJS Encoding
|
|
329
|
-
|
|
330
|
-
api-ape uses **JJS (JSON SuperSet)** encoding, which extends JSON to support:
|
|
331
|
-
|
|
332
|
-
| Type | Supported |
|
|
333
|
-
|------|-----------|
|
|
334
|
-
| `Date` | ✅ Preserved as Date objects |
|
|
335
|
-
| `RegExp` | ✅ Preserved as RegExp |
|
|
336
|
-
| `Error` | ✅ Preserved with name, message, stack |
|
|
337
|
-
| `undefined` | ✅ Preserved (not converted to null) |
|
|
338
|
-
| `Set` | ✅ Preserved as Set |
|
|
339
|
-
| `Map` | ✅ Preserved as Map |
|
|
340
|
-
| Circular refs | ✅ Handled via pointers |
|
|
341
|
-
|
|
342
|
-
This is automatic — send a Date, receive a Date. No configuration needed.
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## HTTP Streaming Fallback
|
|
347
|
-
|
|
348
|
-
api-ape automatically falls back to HTTP streaming when WebSockets are unavailable or blocked by firewalls/proxies.
|
|
349
|
-
|
|
350
|
-
### How It Works
|
|
351
|
-
|
|
352
|
-
1. **WebSocket First**: Client attempts WebSocket connection (4 second timeout)
|
|
353
|
-
2. **Auto-Fallback**: On failure, switches to HTTP streaming transport
|
|
354
|
-
3. **Background Retry**: Keeps attempting WebSocket reconnection every 30 seconds
|
|
355
|
-
4. **Auto-Upgrade**: Switches back to WebSocket when available
|
|
356
|
-
|
|
357
|
-
### Streaming Transport
|
|
358
|
-
|
|
359
|
-
When in polling mode:
|
|
360
|
-
- **GET `/api/ape/poll`**: Long-lived connection, server streams JSON messages
|
|
361
|
-
- **POST `/api/ape/poll`**: Client sends messages
|
|
362
|
-
- Cookie-based session (`apeHostId`) for authentication
|
|
363
|
-
- Heartbeat every 20 seconds to prevent proxy timeout
|
|
364
|
-
|
|
365
|
-
### Force Transport Mode
|
|
366
|
-
|
|
367
|
-
```js
|
|
368
|
-
// Force HTTP streaming (useful for testing)
|
|
369
|
-
ape.configure({ transport: 'polling' })
|
|
370
|
-
|
|
371
|
-
// Force WebSocket only (fail if unavailable)
|
|
372
|
-
ape.configure({ transport: 'websocket' })
|
|
373
|
-
|
|
374
|
-
// Auto-detect (default)
|
|
375
|
-
ape.configure({ transport: 'auto' })
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Check Active Transport
|
|
379
|
-
|
|
380
|
-
```js
|
|
381
|
-
if (ape.getTransport() === 'polling') {
|
|
382
|
-
console.log('Using HTTP streaming fallback')
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
---
|
|
387
|
-
|
|
388
337
|
## Examples & Demos
|
|
389
338
|
|
|
390
339
|
The repository contains working examples:
|
|
391
340
|
|
|
392
|
-
*
|
|
341
|
+
* **[`example/ExpressJs/`](example/ExpressJs/)** — Simple real-time chat app
|
|
393
342
|
- Minimal setup with Express.js
|
|
394
343
|
- Broadcast messages to other clients
|
|
395
344
|
- Message history
|
|
396
345
|
|
|
397
|
-
*
|
|
346
|
+
* **[`example/NextJs/`](example/NextJs/)** — Production-ready chat application
|
|
398
347
|
- Custom Next.js server integration
|
|
399
348
|
- React hooks integration
|
|
400
349
|
- User presence tracking
|
|
401
350
|
- Docker support
|
|
402
351
|
- Connection lifecycle hooks
|
|
403
352
|
|
|
353
|
+
* **[`example/Vite/`](example/Vite/)** — Vite + Vue example
|
|
354
|
+
* **[`example/Bun/`](example/Bun/)** — Bun runtime example
|
|
355
|
+
|
|
404
356
|
### Run an Example
|
|
405
357
|
|
|
406
358
|
**ExpressJs:**
|
|
@@ -427,74 +379,6 @@ docker-compose up --build
|
|
|
427
379
|
|
|
428
380
|
---
|
|
429
381
|
|
|
430
|
-
## Troubleshooting & FAQ
|
|
431
|
-
|
|
432
|
-
### CORS Errors in Browser
|
|
433
|
-
|
|
434
|
-
Ensure your server allows WebSocket connections from your origin. api-ape uses the `ws` library which handles WebSocket upgrades on the HTTP server level.
|
|
435
|
-
|
|
436
|
-
### Controller Not Found
|
|
437
|
-
|
|
438
|
-
* Check that your controller file is in the `where` directory (default: `api/`)
|
|
439
|
-
* Ensure the file exports a function: `module.exports = function(...) { ... }`
|
|
440
|
-
* File paths map directly: `api/users/list.js` → `ape.users.list()`
|
|
441
|
-
|
|
442
|
-
### Connection Drops Frequently
|
|
443
|
-
|
|
444
|
-
The client automatically reconnects with exponential backoff. If connections drop often:
|
|
445
|
-
* Check server WebSocket timeout settings
|
|
446
|
-
* Verify network stability
|
|
447
|
-
* Check server logs for errors
|
|
448
|
-
|
|
449
|
-
### Binary Data / File Transfers
|
|
450
|
-
|
|
451
|
-
api-ape supports transparent binary file transfers. Simply return `Buffer` data from controllers:
|
|
452
|
-
|
|
453
|
-
```js
|
|
454
|
-
// api/files/download.js
|
|
455
|
-
module.exports = function(filename) {
|
|
456
|
-
return {
|
|
457
|
-
name: filename,
|
|
458
|
-
data: fs.readFileSync(`./uploads/${filename}`) // Buffer
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
The client receives `ArrayBuffer` automatically:
|
|
464
|
-
|
|
465
|
-
```js
|
|
466
|
-
const result = await ape.files.download('image.png')
|
|
467
|
-
console.log(result.data) // ArrayBuffer
|
|
468
|
-
|
|
469
|
-
// Display as image
|
|
470
|
-
const blob = new Blob([result.data])
|
|
471
|
-
img.src = URL.createObjectURL(blob)
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
**Uploads work the same way:**
|
|
475
|
-
|
|
476
|
-
```js
|
|
477
|
-
// Client
|
|
478
|
-
const arrayBuffer = await file.arrayBuffer()
|
|
479
|
-
await ape.files.upload({ name: file.name, data: arrayBuffer })
|
|
480
|
-
|
|
481
|
-
// Server (api/files/upload.js)
|
|
482
|
-
module.exports = function({ name, data }) {
|
|
483
|
-
fs.writeFileSync(`./uploads/${name}`, data) // data is Buffer
|
|
484
|
-
return { success: true }
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
Binary data is transferred via temporary HTTP endpoints (`/api/ape/data/:hash`) with session verification and auto-cleanup.
|
|
489
|
-
|
|
490
|
-
### TypeScript Support
|
|
491
|
-
|
|
492
|
-
Type definitions are included (`index.d.ts`). For full type safety, you may need to:
|
|
493
|
-
* Define interfaces for your controller parameters and return types
|
|
494
|
-
* Use type assertions when calling `ape.<path>.<method>()`
|
|
495
|
-
|
|
496
|
-
---
|
|
497
|
-
|
|
498
382
|
## Tests & CI
|
|
499
383
|
|
|
500
384
|
```bash
|
|
@@ -507,7 +391,6 @@ npm run test:cover # Coverage report
|
|
|
507
391
|
- `npm test` — Run all tests
|
|
508
392
|
- `npm run test:watch` — Watch mode for development
|
|
509
393
|
- `npm run test:cover` — Generate coverage report
|
|
510
|
-
- `npm run test:update` — Update snapshots
|
|
511
394
|
|
|
512
395
|
**Supported:** Node.js 14+, modern browsers (Chrome, Firefox, Safari, Edge)
|
|
513
396
|
|
|
@@ -542,11 +425,34 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
542
425
|
|
|
543
426
|
---
|
|
544
427
|
|
|
428
|
+
## Zero Dependencies
|
|
429
|
+
|
|
430
|
+
api-ape has **zero runtime dependencies**. The WebSocket implementation is built-in:
|
|
431
|
+
|
|
432
|
+
- **Node.js 24+**: Uses native `node:ws` module
|
|
433
|
+
- **Bun / Deno**: Uses framework-provided WebSocket support
|
|
434
|
+
- **Earlier Node.js**: Uses built-in RFC 6455 compliant WebSocket server
|
|
435
|
+
|
|
436
|
+
No `npm install` surprises, no dependency audits, no supply chain concerns.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
545
440
|
## Security
|
|
546
441
|
|
|
547
442
|
**Reporting vulnerabilities:** Please report security issues via [GitHub Security Advisories](https://github.com/codemeasandwich/api-ape/security/advisories) or email the maintainer.
|
|
548
443
|
|
|
549
|
-
|
|
444
|
+
### CSRF Protection
|
|
445
|
+
|
|
446
|
+
api-ape includes built-in **Cross-Site Request Forgery (CSRF)** protection via Origin validation:
|
|
447
|
+
|
|
448
|
+
- **Origin Header Check** — Every WebSocket connection validates the `Origin` header against the `Host` header
|
|
449
|
+
- **Automatic Rejection** — Connections from mismatched origins are destroyed immediately
|
|
450
|
+
- **No Configuration Needed** — Protection is enabled by default
|
|
451
|
+
|
|
452
|
+
This prevents malicious sites from making requests to your api-ape server while impersonating logged-in users.
|
|
453
|
+
|
|
454
|
+
### Security Considerations
|
|
455
|
+
|
|
550
456
|
* Validate all input in controllers
|
|
551
457
|
* Use authentication/authorization in `onConnent` hooks
|
|
552
458
|
* Sanitize data before broadcasting
|
|
@@ -559,34 +465,117 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
559
465
|
```
|
|
560
466
|
api-ape/
|
|
561
467
|
├── client/
|
|
562
|
-
│ ├──
|
|
563
|
-
│
|
|
468
|
+
│ ├── index.js # Unified client entry point (auto-buffered)
|
|
469
|
+
│ ├── browser.js # Browser entry point (window.ape)
|
|
470
|
+
│ ├── connectSocket.js # WebSocket client with auto-reconnect
|
|
471
|
+
│ └── transports/
|
|
472
|
+
│ └── streaming.js # HTTP streaming fallback transport
|
|
564
473
|
├── server/
|
|
565
474
|
│ ├── lib/
|
|
566
|
-
│ │ ├── main.js
|
|
567
|
-
│ │ ├── loader.js
|
|
568
|
-
│ │ ├── broadcast.js
|
|
569
|
-
│ │
|
|
475
|
+
│ │ ├── main.js # HTTP server integration
|
|
476
|
+
│ │ ├── loader.js # Auto-loads controller files
|
|
477
|
+
│ │ ├── broadcast.js # Client tracking & broadcast
|
|
478
|
+
│ │ ├── wiring.js # WebSocket handler setup
|
|
479
|
+
│ │ ├── fileTransfer.js # Binary file transfer via HTTP
|
|
480
|
+
│ │ ├── longPolling.js # HTTP streaming fallback handler
|
|
481
|
+
│ │ ├── bun.js # Bun runtime support
|
|
482
|
+
│ │ ├── wsProvider.js # WebSocket provider abstraction
|
|
483
|
+
│ │ └── ws/ # Native WebSocket implementation
|
|
570
484
|
│ ├── socket/
|
|
571
|
-
│ │ ├──
|
|
572
|
-
│ │
|
|
573
|
-
│ └──
|
|
574
|
-
│
|
|
485
|
+
│ │ ├── open.js # Connection handler
|
|
486
|
+
│ │ ├── receive.js # Incoming message handler
|
|
487
|
+
│ │ └── send.js # Outgoing message handler
|
|
488
|
+
│ ├── security/
|
|
489
|
+
│ │ ├── reply.js # Duplicate request protection
|
|
490
|
+
│ │ ├── origin.js # Origin validation
|
|
491
|
+
│ │ └── extractRootDomain.js # Domain extraction utility
|
|
492
|
+
│ └── utils/
|
|
493
|
+
│ ├── deepRequire.js # Deep module loader
|
|
494
|
+
│ ├── genId.js # ID generation
|
|
495
|
+
│ └── parseUserAgent.js # Browser/OS/device parser
|
|
575
496
|
├── utils/
|
|
576
|
-
│ ├── jss.js
|
|
577
|
-
│ └── messageHash.js
|
|
497
|
+
│ ├── jss.js # JSON SuperSet encoder/decoder
|
|
498
|
+
│ └── messageHash.js # Request deduplication hashing
|
|
578
499
|
└── example/
|
|
579
|
-
├── ExpressJs/
|
|
580
|
-
|
|
500
|
+
├── ExpressJs/ # Minimal chat app example
|
|
501
|
+
├── NextJs/ # Next.js production example
|
|
502
|
+
├── Vite/ # Vite/Vue example
|
|
503
|
+
└── Bun/ # Bun runtime example
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Troubleshooting & FAQ
|
|
509
|
+
|
|
510
|
+
### CORS Errors in Browser
|
|
511
|
+
|
|
512
|
+
Ensure your server allows WebSocket connections from your origin. api-ape uses the `ws` library which handles WebSocket upgrades on the HTTP server level.
|
|
513
|
+
|
|
514
|
+
### Controller Not Found
|
|
515
|
+
|
|
516
|
+
* Check that your controller file is in the `where` directory (default: `api/`)
|
|
517
|
+
* Ensure the file exports a function: `module.exports = function(...) { ... }`
|
|
518
|
+
* File paths map directly: `api/users/list.js` → `api.users.list()`
|
|
519
|
+
|
|
520
|
+
### Connection Drops Frequently
|
|
521
|
+
|
|
522
|
+
The client automatically reconnects with exponential backoff. If connections drop often:
|
|
523
|
+
* Check server WebSocket timeout settings
|
|
524
|
+
* Verify network stability
|
|
525
|
+
* Check server logs for errors
|
|
526
|
+
|
|
527
|
+
### Binary Data / File Transfers
|
|
528
|
+
|
|
529
|
+
api-ape supports transparent binary file transfers. Simply return `Buffer` data from controllers:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
// api/files/download.js
|
|
533
|
+
module.exports = function(filename) {
|
|
534
|
+
return {
|
|
535
|
+
name: filename,
|
|
536
|
+
data: fs.readFileSync(`./uploads/${filename}`) // Buffer
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The client receives `ArrayBuffer` automatically:
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
const result = await api.files.download('image.png')
|
|
545
|
+
console.log(result.data) // ArrayBuffer
|
|
546
|
+
|
|
547
|
+
// Display as image
|
|
548
|
+
const blob = new Blob([result.data])
|
|
549
|
+
img.src = URL.createObjectURL(blob)
|
|
581
550
|
```
|
|
582
551
|
|
|
552
|
+
**Uploads work the same way:**
|
|
553
|
+
|
|
554
|
+
```js
|
|
555
|
+
// Client
|
|
556
|
+
const arrayBuffer = await file.arrayBuffer()
|
|
557
|
+
await api.files.upload({ name: file.name, data: arrayBuffer })
|
|
558
|
+
|
|
559
|
+
// Server (api/files/upload.js)
|
|
560
|
+
module.exports = function({ name, data }) {
|
|
561
|
+
fs.writeFileSync(`./uploads/${name}`, data) // data is Buffer
|
|
562
|
+
return { success: true }
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### TypeScript Support
|
|
567
|
+
|
|
568
|
+
Type definitions are included (`index.d.ts`). For full type safety, you may need to:
|
|
569
|
+
* Define interfaces for your controller parameters and return types
|
|
570
|
+
* Use type assertions when calling `api.<path>.<method>()`
|
|
571
|
+
|
|
583
572
|
---
|
|
584
573
|
|
|
585
574
|
## License & Authors
|
|
586
575
|
|
|
587
576
|
**License:** MIT
|
|
588
577
|
|
|
589
|
-
**Author:** [Brian Shannon](https://
|
|
578
|
+
**Author:** [Brian Shannon](https://www.linkedin.com/in/brianshann/)
|
|
590
579
|
|
|
591
580
|
**Repository:** [github.com/codemeasandwich/api-ape](https://github.com/codemeasandwich/api-ape)
|
|
592
581
|
|
package/client/README.md
CHANGED
|
@@ -6,7 +6,8 @@ WebSocket client library with auto-reconnection and proxy-based API calls.
|
|
|
6
6
|
|
|
7
7
|
| File | Description |
|
|
8
8
|
|------|-------------|
|
|
9
|
-
| `
|
|
9
|
+
| `index.js` | **Unified entry** — auto-initializing client with call buffering |
|
|
10
|
+
| `browser.js` | Browser entry point — exposes `window.ape` |
|
|
10
11
|
| `connectSocket.js` | WebSocket client with auto-reconnect, queuing, and JJS encoding |
|
|
11
12
|
|
|
12
13
|
## Usage
|
|
@@ -17,36 +18,44 @@ WebSocket client library with auto-reconnection and proxy-based API calls.
|
|
|
17
18
|
<script src="/api/ape.js"></script>
|
|
18
19
|
<script>
|
|
19
20
|
// Call server functions
|
|
20
|
-
|
|
21
|
+
api.hello('World').then(result => console.log(result))
|
|
21
22
|
|
|
22
23
|
// Listen for broadcasts
|
|
23
|
-
|
|
24
|
+
api.on('message', ({ data }) => console.log(data))
|
|
24
25
|
</script>
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
### ES Module Import
|
|
28
|
+
### ES Module Import (React, Vue, etc.)
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
npm i api-ape
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
```js
|
|
34
|
-
import
|
|
35
|
+
import api from 'api-ape'
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Connect and enable auto-reconnect
|
|
40
|
-
const { sender, setOnReciver } = ape()
|
|
41
|
-
ape.autoReconnect()
|
|
42
|
-
|
|
43
|
-
// Use sender as API
|
|
44
|
-
sender.users.list().then(users => ...)
|
|
37
|
+
// Just use it! Calls are buffered until connected.
|
|
38
|
+
api.users.list().then(users => console.log(users))
|
|
45
39
|
|
|
46
40
|
// Listen for broadcasts
|
|
47
|
-
|
|
41
|
+
api.on('message', ({ data }) => console.log(data))
|
|
42
|
+
|
|
43
|
+
// Track connection state
|
|
44
|
+
api.onConnectionChange((state) => {
|
|
45
|
+
console.log('Connection:', state)
|
|
46
|
+
// 'offline' | 'walled' | 'disconnected' | 'connecting' | 'connected'
|
|
47
|
+
})
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
**Connection States:**
|
|
51
|
+
- `offline` — Browser reports no network
|
|
52
|
+
- `walled` — Captive portal detected (WiFi without real internet)
|
|
53
|
+
- `disconnected` — Had connection, lost it
|
|
54
|
+
- `connecting` — Actively connecting
|
|
55
|
+
- `connected` — Ready to use
|
|
56
|
+
|
|
57
|
+
**No async setup needed!** The client auto-initializes and buffers calls until connected.
|
|
58
|
+
|
|
50
59
|
## Features
|
|
51
60
|
|
|
52
61
|
- **Proxy-based API** — `ape.path.method(data)` converts to WebSocket calls
|
|
@@ -55,19 +64,6 @@ setOnReciver('newUser', ({ data }) => ...)
|
|
|
55
64
|
- **JJS encoding** — Supports Date, RegExp, Error, Set, Map, undefined over the wire
|
|
56
65
|
- **Request timeout** — Configurable timeout (default: 10s)
|
|
57
66
|
|
|
58
|
-
## Configuration
|
|
59
|
-
|
|
60
|
-
```js
|
|
61
|
-
ape.configure({
|
|
62
|
-
port: 3000, // WebSocket port
|
|
63
|
-
host: 'api.example.com' // WebSocket host
|
|
64
|
-
})
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Default port detection:
|
|
68
|
-
- Local (`localhost`, `127.0.0.1`): `9010`
|
|
69
|
-
- Remote: Uses current page port or `443`/`80`
|
|
70
|
-
|
|
71
67
|
## File Transfers
|
|
72
68
|
|
|
73
69
|
Binary data is automatically handled. The client fetches binary resources and uploads binary data transparently.
|
|
@@ -76,7 +72,7 @@ Binary data is automatically handled. The client fetches binary resources and up
|
|
|
76
72
|
|
|
77
73
|
```js
|
|
78
74
|
// Server returns Buffer, client receives ArrayBuffer
|
|
79
|
-
const result = await
|
|
75
|
+
const result = await api.files.download('image.png')
|
|
80
76
|
console.log(result.data) // ArrayBuffer
|
|
81
77
|
|
|
82
78
|
// Display as image
|
|
@@ -91,7 +87,7 @@ const file = input.files[0]
|
|
|
91
87
|
const arrayBuffer = await file.arrayBuffer()
|
|
92
88
|
|
|
93
89
|
// Binary data is uploaded automatically
|
|
94
|
-
await
|
|
90
|
+
await api.files.upload({
|
|
95
91
|
name: file.name,
|
|
96
92
|
data: arrayBuffer // Sent via HTTP PUT
|
|
97
93
|
})
|
|
@@ -99,3 +95,14 @@ await ape.files.upload({
|
|
|
99
95
|
|
|
100
96
|
Binary transfers use `/api/ape/data/:hash` endpoints with session verification.
|
|
101
97
|
|
|
98
|
+
## Security
|
|
99
|
+
|
|
100
|
+
### CSRF Protection
|
|
101
|
+
|
|
102
|
+
api-ape includes built-in **Cross-Site Request Forgery (CSRF)** protection:
|
|
103
|
+
|
|
104
|
+
- **Origin Validation** — WebSocket connections validate Origin header against Host
|
|
105
|
+
- **Automatic Rejection** — Mismatched origins are rejected immediately
|
|
106
|
+
- **Session Verification** — Binary transfers verify session cookies
|
|
107
|
+
|
|
108
|
+
No configuration needed — protection is enabled by default.
|