api-ape 3.0.1 → 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 +58 -570
  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 -202
  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 +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  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 +332 -27
  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 -221
  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 -225
  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 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @fileoverview Binary Data Transfer Plugins for JSS
3
+ *
4
+ * This module provides plugins for handling binary data transfer in api-ape.
5
+ * It moves the binary handling logic from hardcoded server code into the
6
+ * pluggable JSS system.
7
+ *
8
+ * ## Plugin Tags
9
+ *
10
+ * | Tag | Direction | Description |
11
+ * |-----|-----------------|------------------------------------------------|
12
+ * | `I` | Server→Client | Inline base64 for small binary (<=100 chars) |
13
+ * | `L` | Server→Client | Link to downloadable binary data (large) |
14
+ * | `B` | Client→Server | Buffer upload (resolves to Buffer) |
15
+ * | `A` | Client→Server | ArrayBuffer upload (resolves to ArrayBuffer) |
16
+ * | `F` | Client→Client | Streaming file transfer |
17
+ *
18
+ * ## Usage
19
+ *
20
+ * ```javascript
21
+ * const { registerBinaryPlugins } = require('./plugins/binary')
22
+ *
23
+ * // During server initialization
24
+ * registerBinaryPlugins()
25
+ * ```
26
+ *
27
+ * @module server/plugins/binary
28
+ * @see {@link module:utils/jss/plugins} for plugin system
29
+ * @see {@link module:server/lib/fileTransfer} for file transfer management
30
+ */
31
+
32
+ const jss = require("../../utils/jss");
33
+
34
+ /**
35
+ * Maximum size in base64 characters for inline binary encoding
36
+ * Data up to this size will be inlined as base64 in the message.
37
+ * Larger data will use HTTP transfer (L tag).
38
+ *
39
+ * 100 base64 chars ≈ 75 raw bytes
40
+ * @constant {number}
41
+ */
42
+ const INLINE_BASE64_THRESHOLD = 100;
43
+
44
+ /**
45
+ * Check if a value is binary data
46
+ *
47
+ * @param {any} value - Value to check
48
+ * @returns {boolean} True if value is Buffer, ArrayBuffer, or TypedArray
49
+ * @private
50
+ */
51
+ function isBinaryData(value) {
52
+ if (value === null || value === undefined) return false;
53
+ return (
54
+ Buffer.isBuffer(value) ||
55
+ value instanceof ArrayBuffer ||
56
+ ArrayBuffer.isView(value)
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Get the base64 encoded length for binary data
62
+ *
63
+ * @param {Buffer|ArrayBuffer|ArrayBufferView} value - Binary data
64
+ * @returns {number} Length when encoded as base64
65
+ * @private
66
+ */
67
+ function getBase64Length(value) {
68
+ let byteLength;
69
+ if (Buffer.isBuffer(value)) {
70
+ byteLength = value.length;
71
+ } else if (value instanceof ArrayBuffer) {
72
+ byteLength = value.byteLength;
73
+ } else if (ArrayBuffer.isView(value)) {
74
+ byteLength = value.byteLength;
75
+ } else {
76
+ return Infinity; // Unknown type, use HTTP transfer
77
+ }
78
+ // Base64 encoding increases size by ~33%
79
+ return Math.ceil((byteLength * 4) / 3);
80
+ }
81
+
82
+ /**
83
+ * Register binary data plugins for server-side use
84
+ *
85
+ * Call this during server initialization to enable binary transfer
86
+ * functionality through the JSS plugin system.
87
+ *
88
+ * @example
89
+ * const { registerBinaryPlugins } = require('./plugins/binary')
90
+ * registerBinaryPlugins()
91
+ *
92
+ * // Now binary data in controller responses will be handled automatically
93
+ * // Controller: return { image: Buffer.from(...) }
94
+ * // Client receives: { 'image<!L>': 'hash123' }
95
+ */
96
+ function registerBinaryPlugins() {
97
+ const { FileTransferManager } = require("../lib/fileTransfer");
98
+
99
+ /**
100
+ * I tag: Inline base64 for small binary data
101
+ *
102
+ * For small binary data (<=100 base64 chars / ~75 bytes):
103
+ * 1. Converts to base64 inline in the message
104
+ * 2. No HTTP transfer required - reduces latency
105
+ *
106
+ * NOTE: Must be registered BEFORE L tag so it's checked first
107
+ */
108
+ jss.custom("I", {
109
+ // Check if value is SMALL binary data that should be inlined
110
+ check: (key, value) => {
111
+ if (!isBinaryData(value)) return false;
112
+ return getBase64Length(value) <= INLINE_BASE64_THRESHOLD;
113
+ },
114
+
115
+ // Encode: convert to base64 string
116
+ encode: (path, key, value, context) => {
117
+ const buffer = Buffer.isBuffer(value)
118
+ ? value
119
+ : Buffer.from(
120
+ value instanceof ArrayBuffer
121
+ ? value
122
+ : value.buffer.slice(
123
+ value.byteOffset,
124
+ value.byteOffset + value.byteLength,
125
+ ),
126
+ );
127
+ return buffer.toString("base64");
128
+ },
129
+
130
+ // Decode: handled by built-in I decoder in decode.js
131
+ decode: (value, path, context) => Buffer.from(value, "base64"),
132
+
133
+ // No onSend needed - data is inlined, no HTTP transfer
134
+ });
135
+
136
+ /**
137
+ * L tag: Server → Client downloads
138
+ *
139
+ * When a controller returns LARGE binary data (Buffer, ArrayBuffer, TypedArray),
140
+ * this plugin:
141
+ * 1. Registers the data as a pending download
142
+ * 2. Replaces the value with a hash reference
143
+ * 3. Client fetches via HTTP GET /api/ape/data/{hash}
144
+ *
145
+ * NOTE: Small binary data is handled by I tag (inline base64)
146
+ */
147
+ jss.custom("L", {
148
+ // Check if value is LARGE binary data that should be sent as download
149
+ check: (key, value) => {
150
+ if (!isBinaryData(value)) return false;
151
+ return getBase64Length(value) > INLINE_BASE64_THRESHOLD;
152
+ },
153
+
154
+ // Encode: return placeholder (actual value set by onSend)
155
+ encode: (path, key, value, context) => "__pending__",
156
+
157
+ // Decode: on client side, the hash is returned as-is
158
+ // Client will fetch the actual data via HTTP
159
+ decode: (value, path, context) => value,
160
+
161
+ // onSend: register the binary data for HTTP download
162
+ onSend: (path, key, value, context) => {
163
+ const pathStr = path.length > 0 ? path.join(".") : "root";
164
+ const hash = FileTransferManager.generateHash(context.queryId, pathStr);
165
+
166
+ // Detect content type (could be enhanced with magic number detection)
167
+ const contentType = "application/octet-stream";
168
+
169
+ // Register for HTTP download
170
+ context.fileTransfer.registerDownload(
171
+ hash,
172
+ value,
173
+ contentType,
174
+ context.clientId,
175
+ );
176
+
177
+ return { replace: hash };
178
+ },
179
+ });
180
+
181
+ /**
182
+ * B tag: Client → Server Buffer uploads
183
+ *
184
+ * When a client sends binary data with <!B> tag:
185
+ * 1. Server registers an upload expectation
186
+ * 2. Client uploads via HTTP PUT /api/ape/data/{queryId}/{hash}
187
+ * 3. onReceive resolves with the uploaded Buffer
188
+ */
189
+ jss.custom("B", {
190
+ // Check: B tags come from client, we don't check server-side values
191
+ check: () => false,
192
+
193
+ // Encode: not used (B is client→server only)
194
+ encode: (path, key, value, context) => value,
195
+
196
+ // Decode: the hash value is decoded as-is, actual data set by onReceive
197
+ decode: (value, path, context) => value,
198
+
199
+ // onReceive: wait for the binary upload
200
+ onReceive: async (path, key, hash, context) => {
201
+ const uploadData = await context.fileTransfer.registerUpload(
202
+ context.queryId,
203
+ hash,
204
+ context.clientId,
205
+ );
206
+ return uploadData;
207
+ },
208
+ });
209
+
210
+ /**
211
+ * A tag: Client → Server ArrayBuffer uploads
212
+ *
213
+ * Same as B tag but returns ArrayBuffer instead of Buffer.
214
+ */
215
+ jss.custom("A", {
216
+ // Check: A tags come from client, we don't check server-side values
217
+ check: () => false,
218
+
219
+ // Encode: not used (A is client→server only)
220
+ encode: (path, key, value, context) => value,
221
+
222
+ // Decode: the hash value is decoded as-is, actual data set by onReceive
223
+ decode: (value, path, context) => value,
224
+
225
+ // onReceive: wait for the binary upload, convert to ArrayBuffer
226
+ onReceive: async (path, key, hash, context) => {
227
+ const uploadData = await context.fileTransfer.registerUpload(
228
+ context.queryId,
229
+ hash,
230
+ context.clientId,
231
+ );
232
+ // Convert Buffer to ArrayBuffer
233
+ return uploadData.buffer.slice(
234
+ uploadData.byteOffset,
235
+ uploadData.byteOffset + uploadData.byteLength,
236
+ );
237
+ },
238
+ });
239
+
240
+ /**
241
+ * F tag: Client → Client streaming file transfers
242
+ *
243
+ * Used for peer-to-peer file sharing between clients.
244
+ * The server acts as an intermediary, streaming data from
245
+ * sender to receiver without storing the entire file.
246
+ */
247
+ jss.custom("F", {
248
+ // Check: F tags come from client, we don't check server-side values
249
+ check: () => false,
250
+
251
+ // Encode: pass through (F tags are managed by client)
252
+ encode: (path, key, value, context) => value,
253
+
254
+ // Decode: pass through
255
+ decode: (value, path, context) => value,
256
+
257
+ // onReceive: register streaming file expectation
258
+ onReceive: async (path, key, hash, context) => {
259
+ // Register the streaming file to receive uploads
260
+ context.fileTransfer.registerStreamingFile(hash, context.clientId);
261
+ // Return the hash - actual streaming happens via HTTP
262
+ return hash;
263
+ },
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Check if binary plugins are currently registered
269
+ *
270
+ * @returns {boolean} True if the L plugin is registered
271
+ */
272
+ function areBinaryPluginsRegistered() {
273
+ const { hasPlugin } = require("../../utils/jss/plugins");
274
+ return hasPlugin("L");
275
+ }
276
+
277
+ module.exports = {
278
+ registerBinaryPlugins,
279
+ areBinaryPluginsRegistered,
280
+ isBinaryData,
281
+ INLINE_BASE64_THRESHOLD,
282
+ };
@@ -0,0 +1,92 @@
1
+ # Security Module
2
+
3
+ ## Overview
4
+
5
+ This module provides security features to protect api-ape connections from common web vulnerabilities, including Cross-Site Request Forgery (CSRF) protection and duplicate request detection.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **Origin validation** — Verify WebSocket connections originate from the same domain
10
+ - **CSRF protection** — Automatically reject cross-origin WebSocket requests
11
+ - **Replay protection** — Detect and reject duplicate requests within a time window
12
+ - **Domain extraction** — Flexible matching for subdomains and complex hostnames
13
+
14
+ Origin validation is **enabled by default** with no configuration required.
15
+
16
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
17
+
18
+ ## How CSRF Protection Works
19
+
20
+ ```
21
+ ┌─────────────────────────────────────────────────────────────┐
22
+ │ WebSocket Upgrade Request │
23
+ ├─────────────────────────────────────────────────────────────┤
24
+ │ │
25
+ │ Headers: │
26
+ │ Host: example.com │
27
+ │ Origin: https://example.com │
28
+ │ │
29
+ │ origin.js checks: │
30
+ │ 1. Extract domain from Host header │
31
+ │ 2. Extract domain from Origin header │
32
+ │ 3. Compare root domains │
33
+ │ 4. Accept if match, reject if mismatch │
34
+ │ │
35
+ │ ✓ Same origin: example.com === example.com → ALLOWED │
36
+ │ ✗ Cross origin: evil.com !== example.com → REJECTED │
37
+ │ │
38
+ └─────────────────────────────────────────────────────────────┘
39
+ ```
40
+
41
+ ## Why Origin Validation Matters
42
+
43
+ Without origin validation, malicious websites could:
44
+
45
+ 1. Open WebSocket connections to your api-ape server
46
+ 2. Execute API calls using the victim's session cookies
47
+ 3. Access or modify data on behalf of authenticated users
48
+
49
+ Origin validation ensures only your own frontend can establish WebSocket connections.
50
+
51
+ ## Authentication
52
+
53
+ The security module includes a complete authentication system with:
54
+
55
+ - **OPAQUE/PAKE authentication** — Password-authenticated key exchange (server never sees raw password)
56
+ - **Tiered security model** — Guest → Basic → Elevated → High Security
57
+ - **State machine enforcement** — No-downgrade rule, timeout handling, rate limiting
58
+ - **Authorization middleware** — Per-endpoint tier and permission checks
59
+ - **Adapter pattern** — Pluggable authentication methods
60
+
61
+ See [`auth/README.md`](auth/README.md) for full documentation.
62
+
63
+ ### Quick Example
64
+
65
+ ```js
66
+ const { createAuthFramework } = require('api-ape/server/security/auth');
67
+ const { createAuthMiddleware } = require('api-ape/server/socket/authMiddleware');
68
+
69
+ const authFramework = createAuthFramework({
70
+ opaque: {
71
+ getUser: async (username) => db.users.findOne({ username }),
72
+ saveUser: async (username, data) => db.users.insertOne({ username, ...data })
73
+ }
74
+ });
75
+
76
+ const authMiddleware = createAuthMiddleware({
77
+ requirements: {
78
+ 'admin/*': { tier: 2 }, // Admin requires MFA
79
+ 'user/*': { tier: 1 }, // User requires auth
80
+ 'public/*': { tier: 0 } // Public allows guests
81
+ }
82
+ });
83
+
84
+ ape(server, { where: 'api', authFramework, authMiddleware });
85
+ ```
86
+
87
+ ## See Also
88
+
89
+ - [`auth/README.md`](auth/README.md) — Authentication module documentation
90
+ - [`../socket/open.js`](../socket/open.js) — Connection open handler using security
91
+ - [`../lib/wiring.js`](../lib/wiring.js) — WebSocket connection setup
92
+ - [OWASP CSRF Prevention](https://owasp.org/www-community/attacks/csrf) — CSRF attack documentation
@@ -0,0 +1,319 @@
1
+ # Authentication Module
2
+
3
+ ## Overview
4
+
5
+ This module provides a tiered authentication system for api-ape WebSocket connections. It supports OPAQUE-based password authentication where the server never learns raw passwords, with MFA (WebAuthn/TOTP) for elevated security and extensibility for enterprise SSO adapters.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **OPAQUE/PAKE authentication** — Password-authenticated key exchange (server never sees raw password)
10
+ - **MFA support** — WebAuthn (FIDO2) and TOTP (RFC 6238) for Tier 2 elevation
11
+ - **Passport.js compatible** — MFA adapters work with existing Passport.js strategies
12
+ - **Tiered security model** — Guest → Basic → Elevated → High Security
13
+ - **State machine enforcement** — No-downgrade rule, timeout handling, rate limiting
14
+ - **Authorization middleware** — Per-endpoint tier and permission checks
15
+ - **Adapter pattern** — Pluggable authentication methods (OPAQUE, WebAuthn, TOTP, LDAP, SAML, OAuth2)
16
+
17
+ > **Contributing?** See the module files for implementation details.
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ ┌────────────────────────────────────────────────────────────────────┐
23
+ │ AuthFramework │
24
+ │ ┌──────────────────────────────────────────────────────────────┐ │
25
+ │ │ Adapter Registry │ │
26
+ │ │ - OPAQUE (Tier 1) ✓ │ │
27
+ │ │ - WebAuthn (Tier 2 MFA) ✓ │ │
28
+ │ │ - TOTP (Tier 2 MFA) ✓ │ │
29
+ │ │ - LDAP, SAML, OAuth2 (Tier 1, planned) │ │
30
+ │ └──────────────────────────────────────────────────────────────┘ │
31
+ │ ┌──────────────────────────────────────────────────────────────┐ │
32
+ │ │ Per-Socket State Machines │ │
33
+ │ │ - Tracks auth state per clientId │ │
34
+ │ │ - Enforces tier requirements │ │
35
+ │ └──────────────────────────────────────────────────────────────┘ │
36
+ │ ┌──────────────────────────────────────────────────────────────┐ │
37
+ │ │ Message Router │ │
38
+ │ │ - Routes auth messages to appropriate adapter │ │
39
+ │ │ - Handles opaque_*, webauthn_*, totp_*, mfa_* messages │ │
40
+ │ └──────────────────────────────────────────────────────────────┘ │
41
+ └────────────────────────────────────────────────────────────────────┘
42
+ ```
43
+
44
+ ## Authentication Tiers
45
+
46
+ | Tier | Name | Description |
47
+ |------|------|-------------|
48
+ | 0 | GUEST | Unauthenticated, public endpoints only |
49
+ | 1 | BASIC | Identity verified via OPAQUE/SRP or enterprise SSO |
50
+ | 2 | ELEVATED | Tier 1 + MFA (WebAuthn or TOTP) |
51
+ | 3 | HIGH_SECURITY | Full 2-of-3 scheme for client-side key reconstruction |
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Create the Auth Framework
56
+
57
+ ```js
58
+ const { createAuthFramework } = require('api-ape/server/security/auth');
59
+ const { createAuthMiddleware } = require('api-ape/server/socket/authMiddleware');
60
+
61
+ const authFramework = createAuthFramework({
62
+ opaque: {
63
+ // Provide your user storage functions
64
+ getUser: async (username) => db.users.findOne({ username }),
65
+ saveUser: async (username, data) => db.users.insertOne({ username, ...data })
66
+ },
67
+ webauthn: {
68
+ rpId: 'example.com',
69
+ rpName: 'My App',
70
+ // Optional: provide credential storage
71
+ getCredentials: async (userId) => db.webauthn.find({ userId }),
72
+ saveCredential: async (userId, credential) => db.webauthn.insertOne({ userId, ...credential })
73
+ },
74
+ totp: {
75
+ issuer: 'My App',
76
+ // Optional: provide secret storage
77
+ getSecret: async (userId) => db.totp.findOne({ userId }),
78
+ saveSecret: async (userId, data) => db.totp.upsertOne({ userId }, data)
79
+ },
80
+ mfaMethods: ['webauthn', 'totp'],
81
+ onAuthSuccess: (clientId, principal) => {
82
+ console.log(`${clientId} authenticated as ${principal.userId}`);
83
+ },
84
+ onMFASuccess: (clientId, principal, method) => {
85
+ console.log(`${clientId} elevated via ${method}`);
86
+ }
87
+ });
88
+ ```
89
+
90
+ ### 2. Configure Authorization Rules
91
+
92
+ ```js
93
+ const authMiddleware = createAuthMiddleware({
94
+ requirements: {
95
+ 'admin/*': { tier: 2 }, // Admin endpoints require MFA
96
+ 'user/*': { tier: 1 }, // User endpoints require auth
97
+ 'public/*': { tier: 0 } // Public endpoints allow guests
98
+ },
99
+ defaultTier: 0
100
+ });
101
+ ```
102
+
103
+ ### 3. Pass to ape()
104
+
105
+ ```js
106
+ const { ape } = require('api-ape');
107
+
108
+ ape(server, {
109
+ where: 'api',
110
+ authFramework, // Enable authentication
111
+ authMiddleware // Enable authorization
112
+ });
113
+ ```
114
+
115
+ ## Message Protocol
116
+
117
+ ### OPAQUE Registration (Tier 1)
118
+
119
+ ```
120
+ Client Server
121
+ |-- opaque_reg_start ----------->| { user, clientNonce, regRequest }
122
+ |<- opaque_reg_response ---------| { serverNonce, ts, regResponse }
123
+ |-- opaque_reg_finish ---------->| { regRecord }
124
+ |<- opaque_reg_ok ---------------| { msg: "registered" }
125
+ ```
126
+
127
+ ### OPAQUE Authentication (Tier 1)
128
+
129
+ ```
130
+ Client Server
131
+ |-- opaque_auth_start ---------->| { user, clientNonce }
132
+ |<- opaque_auth_1 ---------------| { serverNonce, ts, envelope, oprfResponse }
133
+ |-- opaque_auth_2 -------------->| { clientAuth }
134
+ |<- opaque_auth_ok --------------| { assignedPrincipal, serverProof, tier: 1 }
135
+ ```
136
+
137
+ ### WebAuthn Registration (MFA Setup)
138
+
139
+ ```
140
+ Client Server
141
+ |-- webauthn_reg_start --------->| { userId, userName }
142
+ |<- webauthn_reg_challenge ------| { challenge, rp, user, pubKeyCredParams }
143
+ |-- webauthn_reg_finish -------->| { challenge, attestation }
144
+ |<- webauthn_reg_ok -------------| { credentialId }
145
+ ```
146
+
147
+ ### WebAuthn Authentication (Tier 2 Elevation)
148
+
149
+ ```
150
+ Client Server
151
+ |-- webauthn_auth_start -------->| { userId }
152
+ |<- webauthn_auth_challenge -----| { challenge, allowCredentials }
153
+ |-- webauthn_auth_finish ------->| { challenge, assertion }
154
+ |<- webauthn_auth_ok ------------| { tier: 2 }
155
+ ```
156
+
157
+ ### TOTP Setup
158
+
159
+ ```
160
+ Client Server
161
+ |-- totp_setup_start ----------->| { userId }
162
+ |<- totp_setup_challenge --------| { secret, otpauthUri }
163
+ |-- totp_setup_verify ---------->| { code }
164
+ |<- totp_setup_ok ---------------| { }
165
+ ```
166
+
167
+ ### TOTP Verification (Tier 2 Elevation)
168
+
169
+ ```
170
+ Client Server
171
+ |-- totp_verify ---------------->| { userId, code }
172
+ |<- totp_ok --------------------| { tier: 2 }
173
+ ```
174
+
175
+ ### Generic MFA Challenge Flow
176
+
177
+ ```
178
+ Client Server
179
+ |-- mfa_challenge -------------->| { }
180
+ |<- mfa_challenge ---------------| { methods: [{ method: "totp" }, { method: "webauthn", challenge: {...} }] }
181
+ |-- mfa_verify ----------------->| { method: "totp", code: "123456" }
182
+ |<- mfa_elevated ----------------| { method: "totp", tier: 2 }
183
+ ```
184
+
185
+ ## Passport.js Compatibility
186
+
187
+ Both WebAuthn and TOTP adapters are compatible with Passport.js:
188
+
189
+ ```js
190
+ const passport = require('passport');
191
+ const { WebAuthnStrategy, TOTPStrategy } = require('api-ape/server/security/auth');
192
+
193
+ // Use with Passport.js
194
+ passport.use('webauthn', new WebAuthnStrategy({
195
+ rpId: 'example.com',
196
+ rpName: 'My App'
197
+ }, (user, done) => {
198
+ // Custom verification logic
199
+ done(null, user);
200
+ }));
201
+
202
+ passport.use('totp', new TOTPStrategy({
203
+ issuer: 'My App'
204
+ }, (user, done) => {
205
+ // Custom verification logic
206
+ done(null, user);
207
+ }));
208
+ ```
209
+
210
+ ## Controller Context
211
+
212
+ After authentication, controllers have access to auth state via `this`:
213
+
214
+ ```js
215
+ // api/protected/data.js
216
+ module.exports = function(query) {
217
+ // Check authentication
218
+ if (!this.isAuthenticated) {
219
+ throw new Error('Authentication required');
220
+ }
221
+
222
+ // Access user info
223
+ console.log('User:', this.principal.userId);
224
+ console.log('Roles:', this.principal.roles);
225
+ console.log('Tier:', this.authTier);
226
+
227
+ // Check tier requirement
228
+ if (!this.requiresTier(2)) {
229
+ throw new Error('MFA required for this operation');
230
+ }
231
+
232
+ return { data: 'sensitive info' };
233
+ };
234
+ ```
235
+
236
+ ### Available Properties
237
+
238
+ | Property | Type | Description |
239
+ |----------|------|-------------|
240
+ | `this.isAuthenticated` | `boolean` | Whether socket is authenticated (Tier ≥ 1) |
241
+ | `this.authTier` | `number` | Current tier (0-3) |
242
+ | `this.principal` | `object\|null` | User info: `{ userId, roles, permissions }` |
243
+ | `this.authState` | `object\|null` | Full auth state object |
244
+ | `this.requiresTier(n)` | `function` | Check if socket meets minimum tier |
245
+
246
+ ## Client Tracking
247
+
248
+ Query auth state for any connected client:
249
+
250
+ ```js
251
+ const client = this.clients.get(targetClientId);
252
+
253
+ if (client.isAuthenticated) {
254
+ console.log('User:', client.authState.principal.userId);
255
+ console.log('Tier:', client.authTier);
256
+ }
257
+ ```
258
+
259
+ ## State Machine
260
+
261
+ ```
262
+ GUEST → AUTHENTICATING → AUTHENTICATED (Tier 1)
263
+
264
+ ┌───────────────┼───────────────┐
265
+ │ │ │
266
+ MFA_PENDING KEY_RECOVERY (stay Tier 1)
267
+ │ │
268
+ ▼ ▼
269
+ ELEVATED (2) HIGH_SECURITY (3)
270
+ ```
271
+
272
+ **Rules:**
273
+
274
+ - No downgrade after authentication
275
+ - Higher tiers require completing lower tiers first
276
+ - Auth timeout closes incomplete sessions
277
+ - Rate limiting and lockout after failed attempts
278
+
279
+ ## Security Features
280
+
281
+ | Feature | Description |
282
+ |---------|-------------|
283
+ | **Password protection** | OPAQUE ensures server never sees raw password |
284
+ | **Replay prevention** | Single-use nonces with 30s expiry |
285
+ | **No-downgrade** | Cannot return to lower tier after auth |
286
+ | **Rate limiting** | Lockout after configurable failed attempts |
287
+ | **Session binding** | Auth bound to `clientId + nonces + timestamp` |
288
+ | **TOTP replay protection** | Tracks used counters to prevent code reuse |
289
+ | **WebAuthn counter** | Validates and updates authenticator counter |
290
+
291
+ ## File Structure
292
+
293
+ ```
294
+ auth/
295
+ ├── index.js # Auth framework coordinator
296
+ ├── index.test.js # Integration tests (23 tests)
297
+ ├── state-machine.js # State transitions, tier management
298
+ ├── state-machine.test.js # State machine tests (31 tests)
299
+ ├── nonce-manager.js # Single-use nonce handling
300
+ ├── adapters/
301
+ │ ├── opaque.js # OPAQUE/SRP implementation
302
+ │ ├── opaque.test.js # OPAQUE tests (12 tests)
303
+ │ ├── webauthn.js # WebAuthn/FIDO2 adapter (Passport.js compatible)
304
+ │ ├── webauthn.test.js # WebAuthn tests (25 tests)
305
+ │ ├── totp.js # TOTP RFC 6238 adapter (Passport.js compatible)
306
+ │ └── totp.test.js # TOTP tests (35 tests)
307
+ └── handlers/
308
+ └── auth-messages.js # Message routing
309
+ ```
310
+
311
+ **Total: 114 tests passing**
312
+
313
+ ## See Also
314
+
315
+ - [`../README.md`](../README.md) — Security module overview
316
+ - [`../../socket/authMiddleware.js`](../../socket/authMiddleware.js) — Authorization middleware
317
+ - [`../../socket/receiveContext.js`](../../socket/receiveContext.js) — Controller context with auth
318
+ - [`../../../todo/Security.md`](../../../todo/Security.md) — Security architecture design
319
+ - [`../../../todo/implementation-checklist.md`](../../../todo/implementation-checklist.md) — Implementation progress