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/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,8 +111,9 @@ 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
|
|
116
|
+
* **HTTP streaming fallback** — Automatically falls back to long polling when WebSockets are blocked
|
|
92
117
|
* **JJS Encoding** — Extended JSON supporting Date, RegExp, Error, Set, Map, undefined, and circular refs
|
|
93
118
|
* **Connection lifecycle hooks** — Customize behavior on connect, receive, send, error, and disconnect
|
|
94
119
|
|
|
@@ -117,41 +142,74 @@ Inside controller functions, `this` provides:
|
|
|
117
142
|
| `this.broadcast(type, data)` | Send to **ALL** connected clients |
|
|
118
143
|
| `this.broadcastOthers(type, data)` | Send to all **EXCEPT** the caller |
|
|
119
144
|
| `this.online()` | Get count of connected clients |
|
|
120
|
-
| `this.getClients()` | Get array of connected
|
|
121
|
-
| `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`) |
|
|
122
148
|
| `this.req` | Original HTTP request |
|
|
123
149
|
| `this.socket` | WebSocket instance |
|
|
124
150
|
| `this.agent` | Parsed user-agent (browser, OS, device) |
|
|
125
151
|
|
|
126
152
|
### Client
|
|
127
153
|
|
|
128
|
-
#### `
|
|
154
|
+
#### `api.<path>.<method>(...args)`
|
|
129
155
|
|
|
130
156
|
Call a server function. Returns a Promise.
|
|
131
157
|
|
|
132
158
|
```js
|
|
133
159
|
// Calls api/users/list.js
|
|
134
|
-
const users = await
|
|
160
|
+
const users = await api.users.list()
|
|
135
161
|
|
|
136
162
|
// Calls api/users/create.js with data
|
|
137
|
-
const user = await
|
|
163
|
+
const user = await api.users.create({ name: 'Alice' })
|
|
138
164
|
|
|
139
165
|
// Nested paths work too
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
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)
|
|
143
169
|
```
|
|
144
170
|
|
|
145
|
-
#### `
|
|
171
|
+
#### `api.on(type, handler)`
|
|
146
172
|
|
|
147
173
|
Listen for server broadcasts.
|
|
148
174
|
|
|
149
175
|
```js
|
|
150
|
-
|
|
176
|
+
api.on('notification', ({ data, err, type }) => {
|
|
151
177
|
console.log('Received:', data)
|
|
152
178
|
})
|
|
153
179
|
```
|
|
154
180
|
|
|
181
|
+
#### `api.getTransport()`
|
|
182
|
+
|
|
183
|
+
Get the currently active transport type.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const transport = api.getTransport()
|
|
187
|
+
console.log(transport) // 'websocket' | 'polling' | null
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `api.onConnectionChange(handler)`
|
|
191
|
+
|
|
192
|
+
Listen for connection state changes.
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
const unsubscribe = api.onConnectionChange((state) => {
|
|
196
|
+
console.log('Connection state:', state)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Later: stop listening
|
|
200
|
+
unsubscribe()
|
|
201
|
+
```
|
|
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
|
+
|
|
155
213
|
---
|
|
156
214
|
|
|
157
215
|
## Configuration
|
|
@@ -160,8 +218,7 @@ ape.on('notification', ({ data, err, type }) => {
|
|
|
160
218
|
|
|
161
219
|
```js
|
|
162
220
|
ape(server, {
|
|
163
|
-
where: 'api'
|
|
164
|
-
onConnent: undefined // Lifecycle hook (optional)
|
|
221
|
+
where: 'api' // Controller directory
|
|
165
222
|
})
|
|
166
223
|
```
|
|
167
224
|
|
|
@@ -172,12 +229,12 @@ Customize behavior per connection:
|
|
|
172
229
|
```js
|
|
173
230
|
ape(server, {
|
|
174
231
|
where: 'api',
|
|
175
|
-
onConnent(socket, req,
|
|
232
|
+
onConnent(socket, req, clientId) {
|
|
176
233
|
return {
|
|
177
234
|
// Embed values into `this` for all controllers
|
|
178
235
|
embed: {
|
|
179
236
|
userId: req.session?.userId,
|
|
180
|
-
|
|
237
|
+
id: String(clientId)
|
|
181
238
|
},
|
|
182
239
|
|
|
183
240
|
// Before/after hooks
|
|
@@ -268,7 +325,7 @@ module.exports = async function(id) {
|
|
|
268
325
|
|
|
269
326
|
```js
|
|
270
327
|
try {
|
|
271
|
-
const result = await
|
|
328
|
+
const result = await api.data.get(id)
|
|
272
329
|
console.log(result)
|
|
273
330
|
} catch (err) {
|
|
274
331
|
console.error('Server error:', err)
|
|
@@ -277,40 +334,25 @@ try {
|
|
|
277
334
|
|
|
278
335
|
---
|
|
279
336
|
|
|
280
|
-
## JJS Encoding
|
|
281
|
-
|
|
282
|
-
api-ape uses **JJS (JSON SuperSet)** encoding, which extends JSON to support:
|
|
283
|
-
|
|
284
|
-
| Type | Supported |
|
|
285
|
-
|------|-----------|
|
|
286
|
-
| `Date` | ✅ Preserved as Date objects |
|
|
287
|
-
| `RegExp` | ✅ Preserved as RegExp |
|
|
288
|
-
| `Error` | ✅ Preserved with name, message, stack |
|
|
289
|
-
| `undefined` | ✅ Preserved (not converted to null) |
|
|
290
|
-
| `Set` | ✅ Preserved as Set |
|
|
291
|
-
| `Map` | ✅ Preserved as Map |
|
|
292
|
-
| Circular refs | ✅ Handled via pointers |
|
|
293
|
-
|
|
294
|
-
This is automatic — send a Date, receive a Date. No configuration needed.
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
337
|
## Examples & Demos
|
|
299
338
|
|
|
300
339
|
The repository contains working examples:
|
|
301
340
|
|
|
302
|
-
*
|
|
341
|
+
* **[`example/ExpressJs/`](example/ExpressJs/)** — Simple real-time chat app
|
|
303
342
|
- Minimal setup with Express.js
|
|
304
343
|
- Broadcast messages to other clients
|
|
305
344
|
- Message history
|
|
306
345
|
|
|
307
|
-
*
|
|
346
|
+
* **[`example/NextJs/`](example/NextJs/)** — Production-ready chat application
|
|
308
347
|
- Custom Next.js server integration
|
|
309
348
|
- React hooks integration
|
|
310
349
|
- User presence tracking
|
|
311
350
|
- Docker support
|
|
312
351
|
- Connection lifecycle hooks
|
|
313
352
|
|
|
353
|
+
* **[`example/Vite/`](example/Vite/)** — Vite + Vue example
|
|
354
|
+
* **[`example/Bun/`](example/Bun/)** — Bun runtime example
|
|
355
|
+
|
|
314
356
|
### Run an Example
|
|
315
357
|
|
|
316
358
|
**ExpressJs:**
|
|
@@ -337,74 +379,6 @@ docker-compose up --build
|
|
|
337
379
|
|
|
338
380
|
---
|
|
339
381
|
|
|
340
|
-
## Troubleshooting & FAQ
|
|
341
|
-
|
|
342
|
-
### CORS Errors in Browser
|
|
343
|
-
|
|
344
|
-
Ensure your server allows WebSocket connections from your origin. api-ape uses the `ws` library which handles WebSocket upgrades on the HTTP server level.
|
|
345
|
-
|
|
346
|
-
### Controller Not Found
|
|
347
|
-
|
|
348
|
-
* Check that your controller file is in the `where` directory (default: `api/`)
|
|
349
|
-
* Ensure the file exports a function: `module.exports = function(...) { ... }`
|
|
350
|
-
* File paths map directly: `api/users/list.js` → `ape.users.list()`
|
|
351
|
-
|
|
352
|
-
### Connection Drops Frequently
|
|
353
|
-
|
|
354
|
-
The client automatically reconnects with exponential backoff. If connections drop often:
|
|
355
|
-
* Check server WebSocket timeout settings
|
|
356
|
-
* Verify network stability
|
|
357
|
-
* Check server logs for errors
|
|
358
|
-
|
|
359
|
-
### Binary Data / File Transfers
|
|
360
|
-
|
|
361
|
-
api-ape supports transparent binary file transfers. Simply return `Buffer` data from controllers:
|
|
362
|
-
|
|
363
|
-
```js
|
|
364
|
-
// api/files/download.js
|
|
365
|
-
module.exports = function(filename) {
|
|
366
|
-
return {
|
|
367
|
-
name: filename,
|
|
368
|
-
data: fs.readFileSync(`./uploads/${filename}`) // Buffer
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
The client receives `ArrayBuffer` automatically:
|
|
374
|
-
|
|
375
|
-
```js
|
|
376
|
-
const result = await ape.files.download('image.png')
|
|
377
|
-
console.log(result.data) // ArrayBuffer
|
|
378
|
-
|
|
379
|
-
// Display as image
|
|
380
|
-
const blob = new Blob([result.data])
|
|
381
|
-
img.src = URL.createObjectURL(blob)
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
**Uploads work the same way:**
|
|
385
|
-
|
|
386
|
-
```js
|
|
387
|
-
// Client
|
|
388
|
-
const arrayBuffer = await file.arrayBuffer()
|
|
389
|
-
await ape.files.upload({ name: file.name, data: arrayBuffer })
|
|
390
|
-
|
|
391
|
-
// Server (api/files/upload.js)
|
|
392
|
-
module.exports = function({ name, data }) {
|
|
393
|
-
fs.writeFileSync(`./uploads/${name}`, data) // data is Buffer
|
|
394
|
-
return { success: true }
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Binary data is transferred via temporary HTTP endpoints (`/api/ape/data/:hash`) with session verification and auto-cleanup.
|
|
399
|
-
|
|
400
|
-
### TypeScript Support
|
|
401
|
-
|
|
402
|
-
Type definitions are included (`index.d.ts`). For full type safety, you may need to:
|
|
403
|
-
* Define interfaces for your controller parameters and return types
|
|
404
|
-
* Use type assertions when calling `ape.<path>.<method>()`
|
|
405
|
-
|
|
406
|
-
---
|
|
407
|
-
|
|
408
382
|
## Tests & CI
|
|
409
383
|
|
|
410
384
|
```bash
|
|
@@ -417,7 +391,6 @@ npm run test:cover # Coverage report
|
|
|
417
391
|
- `npm test` — Run all tests
|
|
418
392
|
- `npm run test:watch` — Watch mode for development
|
|
419
393
|
- `npm run test:cover` — Generate coverage report
|
|
420
|
-
- `npm run test:update` — Update snapshots
|
|
421
394
|
|
|
422
395
|
**Supported:** Node.js 14+, modern browsers (Chrome, Firefox, Safari, Edge)
|
|
423
396
|
|
|
@@ -452,11 +425,34 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
452
425
|
|
|
453
426
|
---
|
|
454
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
|
+
|
|
455
440
|
## Security
|
|
456
441
|
|
|
457
442
|
**Reporting vulnerabilities:** Please report security issues via [GitHub Security Advisories](https://github.com/codemeasandwich/api-ape/security/advisories) or email the maintainer.
|
|
458
443
|
|
|
459
|
-
|
|
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
|
+
|
|
460
456
|
* Validate all input in controllers
|
|
461
457
|
* Use authentication/authorization in `onConnent` hooks
|
|
462
458
|
* Sanitize data before broadcasting
|
|
@@ -469,34 +465,117 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
469
465
|
```
|
|
470
466
|
api-ape/
|
|
471
467
|
├── client/
|
|
472
|
-
│ ├──
|
|
473
|
-
│
|
|
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
|
|
474
473
|
├── server/
|
|
475
474
|
│ ├── lib/
|
|
476
|
-
│ │ ├── main.js
|
|
477
|
-
│ │ ├── loader.js
|
|
478
|
-
│ │ ├── broadcast.js
|
|
479
|
-
│ │
|
|
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
|
|
480
484
|
│ ├── socket/
|
|
481
|
-
│ │ ├──
|
|
482
|
-
│ │
|
|
483
|
-
│ └──
|
|
484
|
-
│
|
|
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
|
|
485
496
|
├── utils/
|
|
486
|
-
│ ├── jss.js
|
|
487
|
-
│ └── messageHash.js
|
|
497
|
+
│ ├── jss.js # JSON SuperSet encoder/decoder
|
|
498
|
+
│ └── messageHash.js # Request deduplication hashing
|
|
488
499
|
└── example/
|
|
489
|
-
├── ExpressJs/
|
|
490
|
-
|
|
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)
|
|
491
550
|
```
|
|
492
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
|
+
|
|
493
572
|
---
|
|
494
573
|
|
|
495
574
|
## License & Authors
|
|
496
575
|
|
|
497
576
|
**License:** MIT
|
|
498
577
|
|
|
499
|
-
**Author:** [Brian Shannon](https://
|
|
578
|
+
**Author:** [Brian Shannon](https://www.linkedin.com/in/brianshann/)
|
|
500
579
|
|
|
501
580
|
**Repository:** [github.com/codemeasandwich/api-ape](https://github.com/codemeasandwich/api-ape)
|
|
502
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.
|
package/client/browser.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import connectSocket from './connectSocket.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
5
|
-
connectSocket.configure({ port: parseInt(port, 10) })
|
|
6
|
-
|
|
7
|
-
const { sender, setOnReciver, onConnectionChange } = connectSocket()
|
|
3
|
+
const { sender, setOnReciver, onConnectionChange, getTransport } = connectSocket()
|
|
8
4
|
connectSocket.autoReconnect()
|
|
9
5
|
|
|
10
6
|
// Global API - use defineProperty to bypass Proxy interception
|
|
11
|
-
window.
|
|
12
|
-
Object.defineProperty(window.
|
|
7
|
+
window.api = sender
|
|
8
|
+
Object.defineProperty(window.api, 'on', {
|
|
13
9
|
value: setOnReciver,
|
|
14
10
|
writable: false,
|
|
15
11
|
enumerable: false,
|
|
16
12
|
configurable: false
|
|
17
13
|
})
|
|
18
|
-
Object.defineProperty(window.
|
|
14
|
+
Object.defineProperty(window.api, 'onConnectionChange', {
|
|
19
15
|
value: onConnectionChange,
|
|
20
16
|
writable: false,
|
|
21
17
|
enumerable: false,
|
|
22
18
|
configurable: false
|
|
23
19
|
})
|
|
20
|
+
Object.defineProperty(window.api, 'getTransport', {
|
|
21
|
+
value: getTransport,
|
|
22
|
+
writable: false,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
configurable: false
|
|
25
|
+
})
|