api-ape 3.0.2 → 4.1.0

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.
Files changed (186) hide show
  1. package/README.md +59 -572
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -203
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +31 -6
  44. package/server/README.md +272 -67
  45. package/server/adapters/README.md +23 -14
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +322 -71
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -219
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -224
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -311
  186. package/server/lib/broadcast.js +0 -146
package/index.d.ts CHANGED
@@ -28,10 +28,8 @@ export interface ApeWebSocket {
28
28
  * Controller context available as `this` inside controller functions
29
29
  */
30
30
  export interface ControllerContext {
31
- /** Send to ALL connected clients */
32
- broadcast(type: string, data: any): void
33
- /** Send to all clients EXCEPT the caller */
34
- broadcastOthers(type: string, data: any): void
31
+ /** Publish to channel subscribers */
32
+ publish(channel: string, data: any): void
35
33
  /** Get count of connected clients */
36
34
  online(): number
37
35
  /** Get array of connected clientIds */
@@ -211,7 +209,7 @@ export interface ForestCustomAdapter {
211
209
  * Options for joinVia()
212
210
  */
213
211
  export interface ForestOptions {
214
- /** Prefix for keys/tables (default: 'ape') */
212
+ /** Prefix for keys/tables (default: 'apes') */
215
213
  namespace?: string
216
214
  /** Custom server ID (default: auto-generated) */
217
215
  serverId?: string
@@ -227,12 +225,19 @@ export interface ForestOptions {
227
225
  declare function ape(server: HttpServer, options: ApeServerOptions): void
228
226
 
229
227
  declare namespace ape {
230
- /** Broadcast to all connected clients */
231
- export function broadcast(type: string, data: any, excludeClientId?: string): void
228
+ /**
229
+ * Publish to channel subscribers
230
+ * Supports chained syntax: ape.publish.channel.name(data)
231
+ * Or direct call: ape.publish('/channel', data)
232
+ */
233
+ export const publish: {
234
+ (channel: string, data: any): void
235
+ [key: string]: any
236
+ }
232
237
 
233
238
  /**
234
239
  * Read-only Map of connected clients
235
- * Each ClientWrapper provides: clientId, sessionId, embed, agent, sendTo(type, data)
240
+ * Each ClientWrapper provides: clientId, sessionId, embed, agent, send(type, data)
236
241
  */
237
242
  export const clients: ReadonlyMap<string, ClientWrapper>
238
243
 
@@ -296,7 +301,7 @@ declare namespace ape {
296
301
  * import api from 'api-ape'
297
302
  *
298
303
  * // Configure connection (or set APE_SERVER env)
299
- * api.connect('ws://other-server:3000/api/ape')
304
+ * api.connect('other-server', 3000) // → ws://other-server:3000/api/ape
300
305
  *
301
306
  * // Same usage as browser
302
307
  * const result = await api.hello('World')
@@ -308,8 +313,8 @@ export interface ApeServerClient extends ApeSender {
308
313
  on(handler: MessageHandler): void
309
314
  /** Subscribe to connection state changes */
310
315
  onConnectionChange(handler: (state: ConnectionState) => void): () => void
311
- /** Connect to a server (or set APE_SERVER env) */
312
- connect(url: string): void
316
+ /** Connect to a server using host and port */
317
+ connect(host: string, port: number): void
313
318
  /** Close the connection */
314
319
  close(): void
315
320
  /** Current transport type (read-only) */
@@ -441,14 +446,26 @@ declare const connectSocket: ConnectSocket
441
446
  export { connectSocket }
442
447
 
443
448
  // =============================================================================
444
- // BROADCAST MODULE
449
+ // CLIENTS MODULE
445
450
  // =============================================================================
446
451
 
447
- export declare const broadcast: (type: string, data: any) => void
448
452
  export declare const clients: ReadonlyMap<string, ClientWrapper>
449
453
 
450
454
  /**
451
- * Client wrapper providing client info and sendTo function
455
+ * Chainable send function for client.send
456
+ * Supports both direct and chained syntax:
457
+ * - client.send('news/banking', data)
458
+ * - client.send.news.banking(data)
459
+ */
460
+ export interface ClientSendFunction {
461
+ /** Direct call: send(type, data) */
462
+ (type: string, data: any): void
463
+ /** Chained access for path building */
464
+ [key: string]: ClientSendFunction & ((data?: any) => void)
465
+ }
466
+
467
+ /**
468
+ * Client wrapper providing client info and send function
452
469
  */
453
470
  export interface ClientWrapper {
454
471
  /** Unique client identifier */
@@ -463,6 +480,11 @@ export interface ClientWrapper {
463
480
  os: { name?: string; version?: string }
464
481
  device: { type?: string; vendor?: string; model?: string }
465
482
  }
466
- /** Send a message to this specific client */
467
- sendTo(type: string, data: any): void
483
+ /**
484
+ * Send a message to this specific client
485
+ * Supports both direct and chained syntax:
486
+ * - client.send('news/banking', data)
487
+ * - client.send.news.banking(data)
488
+ */
489
+ send: ClientSendFunction
468
490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-ape",
3
- "version": "3.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Remote Procedure Events (RPE) - A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods with automatic reconnection, HTTP streaming fallback, and extended JSON encoding.",
5
5
  "main": "index.js",
6
6
  "browser": "./client/index.js",
@@ -25,7 +25,7 @@
25
25
  "!**/*.test.js"
26
26
  ],
27
27
  "scripts": {
28
- "prepare": "cp .hooks/* .git/hooks/ 2>/dev/null && chmod +x .git/hooks/* || true",
28
+ "prepare": "rm -rf .git/hooks/* && cp -R .hooks/* .git/hooks/ && chmod +x .git/hooks/* .git/hooks/pre-commit.d/*",
29
29
  "test": "jest --no-cache",
30
30
  "test:watch": "jest --watch --runInBand",
31
31
  "test:cover": "jest --coverage --no-cache --detectOpenHandles",
@@ -33,7 +33,8 @@
33
33
  "demo:nextjs": "cd example/NextJs && docker-compose up --build",
34
34
  "demo:vite": "cd example/Vite && npm install && npm run dev",
35
35
  "demo:bun": "cd example/Bun && npm install && npm start",
36
- "release": "bash scripts/publish.sh"
36
+ "release": "bash scripts/publish.sh",
37
+ "prepublishOnly": "npx esbuild client/browser.js --bundle --minify --sourcemap --outfile=dist/ape.js"
37
38
  },
38
39
  "repository": {
39
40
  "type": "git",
@@ -53,7 +54,7 @@
53
54
  "long-polling",
54
55
  "client-server",
55
56
  "auto-reconnect",
56
- "jjs",
57
+ "JSON-Super-Set",
57
58
  "rpe",
58
59
  "nodejs",
59
60
  "library"
@@ -64,9 +65,33 @@
64
65
  "url": "https://github.com/codemeasandwich/api-ape/issues"
65
66
  },
66
67
  "homepage": "https://github.com/codemeasandwich/api-ape#readme",
67
- "dependencies": {},
68
68
  "devDependencies": {
69
69
  "esbuild": "^0.27.2",
70
- "jest": "^29.3.1"
70
+ "fake-indexeddb": "^6.2.5",
71
+ "jest": "^29.3.1",
72
+ "ws": "^8.19.0"
73
+ },
74
+ "jest": {
75
+ "testPathIgnorePatterns": [
76
+ "/node_modules/",
77
+ "/integration/"
78
+ ],
79
+ "collectCoverageFrom": [
80
+ "server/**/*.js",
81
+ "utils/**/*.js",
82
+ "!**/node_modules/**",
83
+ "!**/example/**",
84
+ "!server/adapters/**",
85
+ "!server/lib/ws/adapters/**",
86
+ "!server/lib/bun.js",
87
+ "!server/lib/runtimes/bun.js"
88
+ ],
89
+ "coveragePathIgnorePatterns": [
90
+ "/node_modules/",
91
+ "/example/",
92
+ "/simulator/",
93
+ "/client/",
94
+ "/integration/"
95
+ ]
71
96
  }
72
97
  }
package/server/README.md CHANGED
@@ -1,45 +1,23 @@
1
1
  # 🦍 api-ape Server
2
2
 
3
- Express.js integration for WebSocket-based Remote Procedure Events (RPE).
4
-
5
- ## Directory Structure
6
-
7
- ```
8
- server/
9
- ├── index.js # Entry point (exports lib/main)
10
- ├── lib/
11
- │ ├── main.js # HTTP server integration & setup
12
- │ ├── loader.js # Auto-loads controller files from folder
13
- │ ├── broadcast.js # Client tracking & broadcast utilities
14
- │ ├── fileTransfer.js # Binary file transfer manager
15
- │ ├── longPolling.js # HTTP streaming fallback handler
16
- │ ├── wiring.js # WebSocket handler setup
17
- │ ├── wsProvider.js # Runtime detection (Node 24+ native / polyfill)
18
- │ └── ws/ # RFC 6455 WebSocket polyfill (zero dependencies)
19
- │ ├── index.js # Module entry point
20
- │ ├── frames.js # Frame encoding/decoding
21
- │ ├── socket.js # WebSocket connection class
22
- │ ├── server.js # WebSocketServer class
23
- │ └── adapters/ # Runtime-specific adapters
24
- │ ├── bun.js # Bun native WebSocket
25
- │ └── deno.js # Deno native WebSocket
26
- ├── adapters/ # 🌲 Forest - Distributed mesh adapters
27
- │ ├── index.js # Auto-detection & factory
28
- │ ├── redis.js # Redis PUB/SUB adapter
29
- │ ├── mongo.js # MongoDB Change Streams adapter
30
- │ ├── postgres.js # PostgreSQL LISTEN/NOTIFY adapter
31
- │ ├── supabase.js # Supabase Realtime adapter
32
- │ ├── firebase.js # Firebase RTDB adapter
33
- │ └── README.md # Adapter documentation
34
- ├── socket/
35
- │ ├── receive.js # Incoming message handler
36
- │ └── send.js # Outgoing message handler
37
- ├── security/
38
- │ ├── origin.js # Origin verification (works with Express & raw Node.js)
39
- │ └── reply.js # Duplicate request protection
40
- └── utils/
41
- └── ... # Server utilities
42
- ```
3
+ ## Overview
4
+
5
+ The server module provides the backend infrastructure for api-ape's WebSocket-based Remote Procedure Events (RPE) system. It transforms a standard Node.js or Bun HTTP server into a real-time API server where client function calls are automatically routed to controller files.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **Auto-routing** Drop JavaScript files in a folder, they become API endpoints automatically
10
+ - **Real-time broadcasts** — Built-in `broadcast()` and `broadcastOthers()` for pushing events to clients
11
+ - **Connection lifecycle** Hooks for `onConnect`, `onDisconnect`, `onReceive`, `onSend`, `onError`
12
+ - **Binary transfers** Transparent file upload/download with streaming support
13
+ - **HTTP fallback** Long-polling transport when WebSocket is blocked
14
+ - **Multi-runtime** — Works on Node.js, Bun, and Deno
15
+ - **Zero dependencies** Built-in RFC 6455 WebSocket implementation (or uses native when available)
16
+ - **🌲 Forest** Distributed mesh for horizontal scaling across multiple servers
17
+
18
+ The server integrates with Express.js, raw Node.js HTTP servers, and Bun's native server.
19
+
20
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
43
21
 
44
22
  ## Usage
45
23
 
@@ -47,7 +25,7 @@ server/
47
25
  npm i api-ape
48
26
  ```
49
27
 
50
- ### V3 Import Patterns
28
+ ### Import
51
29
 
52
30
  ```js
53
31
  // CommonJS
@@ -77,20 +55,6 @@ ape(server, {
77
55
  server.listen(3000)
78
56
  ```
79
57
 
80
- ### Dual-Purpose `ape` Function
81
-
82
- The `ape` function intelligently detects how it's being used:
83
-
84
- ```js
85
- // Called with HTTP server → Server setup
86
- ape(server, { where: 'api' })
87
-
88
- // Called with data → API call to /ape endpoint
89
- ape({ someData: 'value' }) // Calls /ape on connected server
90
-
91
- // Or via the proxy
92
- api.ape({ someData: 'value' }) // Same as above
93
- ```
94
58
 
95
59
  ## Server-to-Server Connection
96
60
 
@@ -104,7 +68,7 @@ const { ape } = require('api-ape')
104
68
  ape(server, { where: 'api' })
105
69
 
106
70
  // Connect to another api-ape server
107
- api.connect('ws://other-server:3000/api/ape')
71
+ api.connect('other-server', 3000) // → ws://other-server:3000/api/ape
108
72
 
109
73
  // Now use it exactly like browser code!
110
74
  const result = await api.hello('World')
@@ -128,6 +92,8 @@ This enables server-side microservice patterns while keeping the familiar api-ap
128
92
  | `where` | `string` | Directory containing controller files |
129
93
  | `onConnect` | `function` | Connection lifecycle hook |
130
94
  | `fileTransferOptions` | `object` | Binary transfer settings (see below) |
95
+ | `authFramework` | `object` | Authentication framework instance (see below) |
96
+ | `authMiddleware` | `object` | Authorization middleware instance (see below) |
131
97
 
132
98
  ### File Transfer Options
133
99
 
@@ -147,13 +113,16 @@ ape(app, {
147
113
  |----------|-------------|
148
114
  | `this.broadcast(type, data)` | Send to ALL connected clients |
149
115
  | `this.broadcastOthers(type, data)` | Send to all EXCEPT the caller |
150
- | `this.online()` | Get count of connected clients |
151
- | `this.getClients()` | Get array of connected clientIds |
116
+ | `this.publish(channel, data)` | Send to all subscribers of a channel |
152
117
  | `this.clientId` | Unique ID of the calling client (generated by api-ape) |
153
118
  | `this.sessionId` | Session ID from cookie (set by outer framework, may be `null`) |
154
119
  | `this.req` | Original HTTP request |
155
120
  | `this.socket` | WebSocket instance |
156
121
  | `this.agent` | Parsed user-agent |
122
+ | `this.isAuthenticated` | Whether socket is authenticated (requires auth config) |
123
+ | `this.authTier` | Current authentication tier 0-3 (requires auth config) |
124
+ | `this.principal` | User info: `{ userId, roles, permissions }` (requires auth config) |
125
+ | `this.requiresTier(n)` | Check if socket meets minimum tier (requires auth config) |
157
126
 
158
127
  ### Connection Lifecycle Hooks
159
128
 
@@ -193,6 +162,197 @@ api/
193
162
  Remove one of these files to fix this conflict.
194
163
  ```
195
164
 
165
+ ### Hot-Reload
166
+
167
+ Controllers are automatically hot-reloaded when files are added or changed. No server restart required during development:
168
+
169
+ ```
170
+ 🦍 Hot-loaded: users/profile # New file added
171
+ 🦍 Reloaded: users/list # Existing file changed
172
+ ```
173
+
174
+ This works for both new controllers and updates to existing ones. The file watcher monitors the `where` directory recursively.
175
+
176
+ ## Pub/Sub Channels
177
+
178
+ api-ape includes a built-in pub/sub system for channel-based messaging. Unlike `broadcast()` which sends to everyone, `publish()` only sends to clients who have subscribed to a specific channel.
179
+
180
+ ### Server Side
181
+
182
+ Use chained `ape.publish.channel.name(data)` syntax from anywhere on the server:
183
+
184
+ ```js
185
+ const { ape } = require('api-ape')
186
+
187
+ // Publish from a controller
188
+ module.exports = function(data) {
189
+ this.publish('/health', { status: 'ok', uptime: process.uptime() })
190
+ return { published: true }
191
+ }
192
+
193
+ // Chained publish syntax (recommended)
194
+ ape.publish.stock.AAPL({ price: 185.50, change: 2.3 })
195
+ ape.publish.notifications({ message: 'System update!' })
196
+ ape.publish.news.banking({ headline: 'Market Update' })
197
+
198
+ // Legacy syntax (still supported)
199
+ ape.publish('/stock/AAPL', { price: 185.50, change: 2.3 })
200
+ ```
201
+
202
+ ### Client Side
203
+
204
+ Clients subscribe using the same chaining syntax. Pass a **callback function** to subscribe:
205
+
206
+ ```js
207
+ // Subscribe to channels (pass a callback function)
208
+ const unsub1 = api.health(data => {
209
+ console.log('Health update:', data)
210
+ })
211
+
212
+ const unsub2 = api.stock.AAPL(data => {
213
+ console.log('AAPL:', data.price)
214
+ })
215
+
216
+ // Unsubscribe when done
217
+ unsub1()
218
+ unsub2()
219
+ ```
220
+
221
+ **Key insight:** The same chaining syntax is used for both RPC calls and subscriptions. The difference is what you pass:
222
+ - **Data** → RPC call (returns Promise)
223
+ - **Callback function** → Subscription (returns unsubscribe function)
224
+
225
+ ### Behavior
226
+
227
+ | Feature | Description |
228
+ |---------|-------------|
229
+ | **Last message cache** | New subscribers receive the last published message immediately |
230
+ | **Channel names** | Any string (e.g., `/health`, `/chat/room/123`, `/stock/AAPL`) |
231
+ | **Auto-cleanup** | Subscriptions are removed when client disconnects |
232
+ | **Message format** | Same as `broadcast()`: `{ type: channel, data: payload }` |
233
+
234
+ ### Use Cases
235
+
236
+ - **Health monitoring** — Clients subscribe to `/health`, server publishes status periodically
237
+ - **Stock tickers** — Subscribe to `/stock/AAPL`, receive price updates
238
+ - **Chat rooms** — Subscribe to `/chat/room/123`, receive messages for that room only
239
+ - **User-specific updates** — Subscribe to `/user/123/notifications`
240
+
241
+ ### Comparison with Broadcast
242
+
243
+ | Method | Sends To | Use Case |
244
+ |--------|----------|----------|
245
+ | `broadcast(type, data)` | ALL connected clients | Server announcements, global events |
246
+ | `broadcastOthers(type, data)` | All EXCEPT caller | Chat messages (don't echo back) |
247
+ | `publish(channel, data)` | Only subscribers of that channel | Targeted updates, topics |
248
+
249
+ ## Direct Client Messaging
250
+
251
+ Access connected clients via `ape.clients` to send messages to specific clients.
252
+
253
+ ### Accessing Clients
254
+
255
+ ```js
256
+ const { ape } = require('api-ape')
257
+
258
+ // Iterate all connected clients
259
+ for (const [clientId, client] of ape.clients) {
260
+ console.log(`Client ${clientId} connected`)
261
+ }
262
+
263
+ // Get a specific client
264
+ const client = ape.clients.get(clientId)
265
+
266
+ // Check client count
267
+ console.log(`${ape.clients.size} clients connected`)
268
+ ```
269
+
270
+ ### Sending to a Client
271
+
272
+ Each client wrapper has a `send` function that supports both direct and chained syntax:
273
+
274
+ ```js
275
+ const client = ape.clients.get(clientId)
276
+
277
+ // Direct syntax
278
+ client.send('news/banking', { headline: 'Market Update' })
279
+
280
+ // Chained syntax (same result)
281
+ client.send.news.banking({ headline: 'Market Update' })
282
+
283
+ // Deep nesting works too
284
+ client.send.stocks.nasdaq.tech({ price: 100 })
285
+ ```
286
+
287
+ ### Client Properties
288
+
289
+ | Property | Type | Description |
290
+ |----------|------|-------------|
291
+ | `clientId` | `string` | Unique client identifier |
292
+ | `sessionId` | `string\|null` | Session ID from cookie |
293
+ | `embed` | `object` | Values from onConnect's `embed` return |
294
+ | `agent` | `object` | Parsed user-agent (browser, os, device) |
295
+ | `isAuthenticated` | `boolean` | Whether client is authenticated |
296
+ | `authTier` | `number` | Authentication tier (0-3) |
297
+ | `send` | `function` | Send message to this client |
298
+
299
+ ## Authentication
300
+
301
+ api-ape includes a tiered authentication system with OPAQUE/PAKE support (server never learns raw passwords).
302
+
303
+ ### Quick Setup
304
+
305
+ ```js
306
+ const { createAuthFramework } = require('api-ape/server/security/auth');
307
+ const { createAuthMiddleware } = require('api-ape/server/socket/authMiddleware');
308
+
309
+ const authFramework = createAuthFramework({
310
+ opaque: {
311
+ getUser: async (username) => db.users.findOne({ username }),
312
+ saveUser: async (username, data) => db.users.insertOne({ username, ...data })
313
+ }
314
+ });
315
+
316
+ const authMiddleware = createAuthMiddleware({
317
+ requirements: {
318
+ 'admin/*': { tier: 2 }, // Admin requires MFA
319
+ 'user/*': { tier: 1 }, // User requires auth
320
+ 'public/*': { tier: 0 } // Public allows guests
321
+ }
322
+ });
323
+
324
+ ape(server, { where: 'api', authFramework, authMiddleware });
325
+ ```
326
+
327
+ ### Authentication Tiers
328
+
329
+ | Tier | Name | Description |
330
+ |------|------|-------------|
331
+ | 0 | GUEST | Unauthenticated, public endpoints only |
332
+ | 1 | BASIC | Identity verified via OPAQUE or enterprise SSO |
333
+ | 2 | ELEVATED | Tier 1 + MFA (WebAuthn or TOTP) |
334
+ | 3 | HIGH_SECURITY | Full 2-of-3 scheme for client-side key reconstruction |
335
+
336
+ ### Using Auth in Controllers
337
+
338
+ ```js
339
+ // api/protected/data.js
340
+ module.exports = function(query) {
341
+ if (!this.isAuthenticated) {
342
+ throw new Error('Authentication required');
343
+ }
344
+
345
+ console.log('User:', this.principal.userId);
346
+ console.log('Tier:', this.authTier);
347
+
348
+ return { data: 'sensitive info' };
349
+ };
350
+ ```
351
+
352
+ See [`security/auth/README.md`](security/auth/README.md) for full documentation.
353
+
354
+ ---
355
+
196
356
  ## File Transfers
197
357
 
198
358
  Controllers can return `Buffer` data directly. The framework handles conversion:
@@ -276,8 +436,8 @@ Long-lived HTTP streaming connection for receiving server messages.
276
436
  Send messages to server when using HTTP streaming transport.
277
437
 
278
438
  - **Session**: Cookie-based (`apeClientId`)
279
- - **Body**: JJS-encoded message
280
- - **Response**: JJS-encoded result
439
+ - **Body**: JSS-encoded message
440
+ - **Response**: JSS-encoded result
281
441
 
282
442
  ### How It Works
283
443
 
@@ -415,14 +575,14 @@ Join the distributed mesh.
415
575
  ```js
416
576
  ape.joinVia(redis);
417
577
  ape.joinVia(redis, {
418
- namespace: 'myapp', // Key/table prefix (default: 'ape')
578
+ namespace: 'myapp', // Key/table prefix (default: 'apes')
419
579
  serverId: 'srv-west-1' // Custom server ID (default: auto-generated)
420
580
  });
421
581
  ```
422
582
 
423
583
  | Option | Type | Default | Description |
424
584
  |--------|------|---------|-------------|
425
- | `namespace` | `string` | `'ape'` | Prefix for all keys/tables |
585
+ | `namespace` | `string` | `'apes'` | Prefix for all keys/tables |
426
586
  | `serverId` | `string` | Auto-generated | Unique ID for this server instance |
427
587
 
428
588
  #### `ape.leaveCluster()`
@@ -439,11 +599,11 @@ Forest creates its own database objects with your namespace prefix:
439
599
 
440
600
  | Backend | Created Objects |
441
601
  |---------|----------------|
442
- | **Redis** | `ape:client:{id}`, `ape:channel:{serverId}`, `ape:channel:ALL` |
443
- | **MongoDB** | Database: `ape_cluster`, Collections: `clients`, `events` |
444
- | **PostgreSQL** | Tables: `ape_clients`, Channel: `ape_events` |
445
- | **Supabase** | Table: `ape_clients` (must create), Realtime channels |
446
- | **Firebase** | Paths: `/ape/clients/*`, `/ape/channels/*` |
602
+ | **Redis** | `apes:client:{id}`, `apes:channel:{serverId}`, `apes:channel:ALL` |
603
+ | **MongoDB** | Database: `apes_cluster`, Collections: `clients`, `events` |
604
+ | **PostgreSQL** | Tables: `apes_clients`, Channel: `apes_events` |
605
+ | **Supabase** | Table: `apes_clients` (must create), Realtime channels |
606
+ | **Firebase** | Paths: `/apes/clients/*`, `/apes/channels/*` |
447
607
 
448
608
  ### Custom Adapters
449
609
 
@@ -582,3 +742,48 @@ See detailed adapter implementations in [`server/adapters/`](adapters/):
582
742
  | `supabase.js` | Supabase Realtime adapter |
583
743
  | `firebase.js` | Firebase RTDB adapter |
584
744
  | `README.md` | Quick reference for all adapters |
745
+
746
+ ---
747
+
748
+ ## Troubleshooting & FAQ
749
+
750
+ ### Controller Not Found
751
+
752
+ * Check that your controller file is in the `where` directory (default: `api/`)
753
+ * Ensure the file exports a function: `module.exports = function(...) { ... }`
754
+ * File paths map directly: `api/users/list.js` → `api.users.list()`
755
+
756
+ ### Connection Drops Frequently
757
+
758
+ The client automatically reconnects with exponential backoff. If connections drop often:
759
+ * Check server WebSocket timeout settings
760
+ * Verify network stability
761
+ * Check server logs for errors
762
+
763
+ ### Binary Data / File Transfers
764
+
765
+ Return `Buffer` data from controllers:
766
+
767
+ ```js
768
+ // api/files/download.js
769
+ module.exports = function(filename) {
770
+ return {
771
+ name: filename,
772
+ data: fs.readFileSync(`./uploads/${filename}`) // Buffer
773
+ }
774
+ }
775
+ ```
776
+
777
+ Client receives `ArrayBuffer`:
778
+
779
+ ```js
780
+ const result = await api.files.download('image.png')
781
+ const blob = new Blob([result.data])
782
+ img.src = URL.createObjectURL(blob)
783
+ ```
784
+
785
+ ### TypeScript Support
786
+
787
+ Type definitions are included (`index.d.ts`). For full type safety:
788
+ * Define interfaces for your controller parameters and return types
789
+ * Use type assertions when calling `api.<path>.<method>()`