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
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @fileoverview Nonce Manager for api-ape Authentication
3
+ *
4
+ * Handles single-use server nonce generation, validation, and consumption.
5
+ *
6
+ * @module server/security/auth/nonce-manager
7
+ * @see {@link module:server/security/auth/state-machine} for the main state machine
8
+ */
9
+
10
+ const crypto = require("crypto");
11
+
12
+ /**
13
+ * Create nonce management functions
14
+ *
15
+ * @param {Object} deps - Dependencies
16
+ * @param {number} deps.nonceExpiry - Nonce expiry time in ms
17
+ * @param {Object} deps.AuthError - Error code enum
18
+ * @returns {Object} Nonce management functions
19
+ */
20
+ function createNonceManager(deps) {
21
+ const { nonceExpiry, AuthError } = deps;
22
+
23
+ const usedNonces = new Set();
24
+ const pendingNonces = new Map();
25
+
26
+ /**
27
+ * Generate a single-use server nonce
28
+ * @param {number} [length=32] - Nonce length in bytes
29
+ * @returns {Object} Nonce info { nonce, expiresAt }
30
+ */
31
+ function generateNonce(length = 32) {
32
+ const nonce = crypto.randomBytes(length).toString("base64url");
33
+ const expiresAt = Date.now() + nonceExpiry;
34
+ pendingNonces.set(nonce, { expiresAt, used: false });
35
+ setTimeout(() => {
36
+ pendingNonces.delete(nonce);
37
+ }, nonceExpiry + 1000);
38
+ return { nonce, expiresAt };
39
+ }
40
+
41
+ /**
42
+ * Validate and consume a nonce
43
+ * @param {string} nonce - The nonce to validate
44
+ * @returns {boolean} True if valid
45
+ * @throws {Error} If nonce is expired, reused, or invalid
46
+ */
47
+ function consumeNonce(nonce) {
48
+ if (usedNonces.has(nonce)) {
49
+ const err = new Error("Nonce already used");
50
+ err.code = AuthError.NONCE_REUSED;
51
+ throw err;
52
+ }
53
+ const nonceInfo = pendingNonces.get(nonce);
54
+ if (!nonceInfo) {
55
+ const err = new Error("Invalid or expired nonce");
56
+ err.code = AuthError.NONCE_EXPIRED;
57
+ throw err;
58
+ }
59
+ if (Date.now() > nonceInfo.expiresAt) {
60
+ pendingNonces.delete(nonce);
61
+ const err = new Error("Nonce expired");
62
+ err.code = AuthError.NONCE_EXPIRED;
63
+ throw err;
64
+ }
65
+ pendingNonces.delete(nonce);
66
+ usedNonces.add(nonce);
67
+ setTimeout(() => {
68
+ usedNonces.delete(nonce);
69
+ }, nonceExpiry * 2);
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Clear all pending nonces
75
+ */
76
+ function clearPendingNonces() {
77
+ pendingNonces.clear();
78
+ }
79
+
80
+ return {
81
+ generateNonce,
82
+ consumeNonce,
83
+ clearPendingNonces,
84
+ };
85
+ }
86
+
87
+ module.exports = {
88
+ createNonceManager,
89
+ };
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @fileoverview MFA Elevation Module for api-ape Server
3
+ *
4
+ * Provides MFA (Multi-Factor Authentication) elevation functions
5
+ * and Key Recovery (2-of-3 Tier 3) elevation for the authentication state machine.
6
+ *
7
+ * @module server/security/auth/state-machine-mfa
8
+ * @see {@link module:server/security/auth/state-machine} for the main state machine
9
+ */
10
+
11
+ /**
12
+ * Create MFA elevation functions
13
+ *
14
+ * @param {Object} deps - Dependencies
15
+ * @param {Function} deps.getState - Get current state function
16
+ * @param {Function} deps.getTier - Get current tier function
17
+ * @param {Function} deps.transition - State transition function
18
+ * @param {Function} deps.generateNonce - Nonce generator
19
+ * @param {Function} deps.getPrincipal - Get principal function
20
+ * @param {Function} deps.setPrincipal - Set principal function
21
+ * @param {Object} deps.AuthState - State enum
22
+ * @param {Object} deps.AuthTier - Tier enum
23
+ * @param {Object} deps.AuthError - Error enum
24
+ * @returns {Object} MFA functions
25
+ */
26
+ function createMFAFunctions(deps) {
27
+ const {
28
+ getState,
29
+ getTier,
30
+ transition,
31
+ generateNonce,
32
+ getPrincipal,
33
+ setPrincipal,
34
+ AuthState,
35
+ AuthTier,
36
+ AuthError,
37
+ } = deps;
38
+
39
+ // Track pending key recovery challenges
40
+ let pendingKeyRecovery = null;
41
+
42
+ /**
43
+ * Start MFA elevation flow
44
+ *
45
+ * @param {string[]} methods - Available MFA methods
46
+ * @throws {Error} If not at Tier 1
47
+ * @returns {Object} MFA challenge info
48
+ */
49
+ function startMFA(methods) {
50
+ const state = getState();
51
+ if (state.state !== AuthState.AUTHENTICATED) {
52
+ const err = new Error("Must be authenticated before MFA");
53
+ err.code = AuthError.INVALID_TRANSITION;
54
+ throw err;
55
+ }
56
+
57
+ transition(AuthState.MFA_PENDING);
58
+
59
+ return {
60
+ state: AuthState.MFA_PENDING,
61
+ methods,
62
+ challenge: generateNonce(),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Complete MFA elevation
68
+ *
69
+ * @param {string} method - MFA method used
70
+ * @returns {Object} Elevation result
71
+ */
72
+ function completeMFA(method) {
73
+ const state = getState();
74
+ if (state.state !== AuthState.MFA_PENDING) {
75
+ const err = new Error("Not in MFA pending state");
76
+ err.code = AuthError.INVALID_TRANSITION;
77
+ throw err;
78
+ }
79
+
80
+ transition(AuthState.ELEVATED);
81
+
82
+ const principal = getPrincipal();
83
+ principal.elevatedAt = Date.now();
84
+ principal.mfaMethod = method;
85
+ setPrincipal(principal);
86
+
87
+ return {
88
+ state: AuthState.ELEVATED,
89
+ tier: getTier(),
90
+ principal,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Get current auth state snapshot
96
+ * @returns {Object} Current state info
97
+ */
98
+ function getStateSnapshot() {
99
+ const state = getState();
100
+ const principal = getPrincipal();
101
+ const tier = getTier();
102
+ return {
103
+ state: state.state,
104
+ tier,
105
+ principal: principal ? { ...principal } : null,
106
+ isAuthenticated: tier >= AuthTier.BASIC,
107
+ isElevated: tier >= AuthTier.ELEVATED,
108
+ isHighSecurity: tier >= AuthTier.HIGH_SECURITY,
109
+ keyRecoveryPending: pendingKeyRecovery !== null,
110
+ };
111
+ }
112
+
113
+ // ============================================================
114
+ // Key Recovery (2-of-3 Tier 3) Functions
115
+ // ============================================================
116
+
117
+ /**
118
+ * Start key recovery elevation flow
119
+ * Requires ELEVATED tier (Tier 2)
120
+ *
121
+ * @param {Object} options - Key recovery options
122
+ * @param {string[]} options.factors - Available factors ['oauth', 'webauthn', 'totp']
123
+ * @returns {Object} Key recovery challenge info
124
+ * @throws {Error} If not at ELEVATED tier
125
+ */
126
+ function startKeyRecovery(options = {}) {
127
+ const state = getState();
128
+
129
+ // Can start from AUTHENTICATED or ELEVATED
130
+ if (state.state !== AuthState.ELEVATED && state.state !== AuthState.AUTHENTICATED) {
131
+ const err = new Error("Must be authenticated or elevated before key recovery");
132
+ err.code = AuthError.INVALID_TRANSITION;
133
+ throw err;
134
+ }
135
+
136
+ // If starting from AUTHENTICATED, automatically elevate for key recovery
137
+ // This handles direct Tier 1 -> Tier 3 path
138
+ if (state.state === AuthState.AUTHENTICATED) {
139
+ // Key recovery can start from AUTHENTICATED per VALID_TRANSITIONS
140
+ }
141
+
142
+ transition(AuthState.KEY_RECOVERY_PENDING);
143
+
144
+ const challenge = generateNonce();
145
+ pendingKeyRecovery = {
146
+ challenge,
147
+ startedAt: Date.now(),
148
+ factors: options.factors || ['oauth', 'webauthn', 'totp'],
149
+ verifiedFactors: [],
150
+ };
151
+
152
+ return {
153
+ state: AuthState.KEY_RECOVERY_PENDING,
154
+ challenge,
155
+ factors: pendingKeyRecovery.factors,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Complete key recovery with proof
161
+ *
162
+ * @param {Object} params - Completion parameters
163
+ * @param {string} params.proof - HMAC proof that client reconstructed K_user
164
+ * @param {string[]} params.usedFactors - The two factors used for recovery
165
+ * @returns {Object} Elevation result
166
+ * @throws {Error} If not in KEY_RECOVERY_PENDING state or proof invalid
167
+ */
168
+ function completeKeyRecovery(params) {
169
+ const { proof, usedFactors } = params;
170
+
171
+ const state = getState();
172
+ if (state.state !== AuthState.KEY_RECOVERY_PENDING) {
173
+ const err = new Error("Not in key recovery pending state");
174
+ err.code = AuthError.INVALID_TRANSITION;
175
+ throw err;
176
+ }
177
+
178
+ if (!pendingKeyRecovery) {
179
+ const err = new Error("No pending key recovery challenge");
180
+ err.code = AuthError.INVALID_TRANSITION;
181
+ throw err;
182
+ }
183
+
184
+ if (!proof || typeof proof !== 'string') {
185
+ const err = new Error("Invalid key recovery proof");
186
+ err.code = AuthError.INVALID_PROOF;
187
+ throw err;
188
+ }
189
+
190
+ if (!Array.isArray(usedFactors) || usedFactors.length !== 2) {
191
+ const err = new Error("Must use exactly 2 factors for key recovery");
192
+ err.code = AuthError.INVALID_PROOF;
193
+ throw err;
194
+ }
195
+
196
+ // Note: Actual proof verification happens in the two-of-three adapter
197
+ // The state machine just manages the state transitions
198
+
199
+ transition(AuthState.HIGH_SECURITY);
200
+
201
+ const principal = getPrincipal();
202
+ principal.highSecurityAt = Date.now();
203
+ principal.keyRecoveryFactors = usedFactors;
204
+ principal.tier = AuthTier.HIGH_SECURITY;
205
+ setPrincipal(principal);
206
+
207
+ // Clear pending recovery
208
+ pendingKeyRecovery = null;
209
+
210
+ return {
211
+ state: AuthState.HIGH_SECURITY,
212
+ tier: getTier(),
213
+ principal,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Cancel pending key recovery
219
+ * Returns to ELEVATED state
220
+ *
221
+ * @returns {Object} New state info
222
+ */
223
+ function cancelKeyRecovery() {
224
+ const state = getState();
225
+ if (state.state !== AuthState.KEY_RECOVERY_PENDING) {
226
+ const err = new Error("No key recovery in progress");
227
+ err.code = AuthError.INVALID_TRANSITION;
228
+ throw err;
229
+ }
230
+
231
+ transition(AuthState.ELEVATED);
232
+ pendingKeyRecovery = null;
233
+
234
+ return {
235
+ state: AuthState.ELEVATED,
236
+ tier: getTier(),
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Get pending key recovery status
242
+ * @returns {Object|null} Pending recovery info or null
243
+ */
244
+ function getKeyRecoveryStatus() {
245
+ if (!pendingKeyRecovery) return null;
246
+
247
+ return {
248
+ challenge: pendingKeyRecovery.challenge,
249
+ factors: pendingKeyRecovery.factors,
250
+ startedAt: pendingKeyRecovery.startedAt,
251
+ verifiedFactors: pendingKeyRecovery.verifiedFactors,
252
+ };
253
+ }
254
+
255
+ return {
256
+ startMFA,
257
+ completeMFA,
258
+ getStateSnapshot,
259
+ // Key recovery functions
260
+ startKeyRecovery,
261
+ completeKeyRecovery,
262
+ cancelKeyRecovery,
263
+ getKeyRecoveryStatus,
264
+ };
265
+ }
266
+
267
+ module.exports = {
268
+ createMFAFunctions,
269
+ };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @fileoverview Authentication State Machine for api-ape Server
3
+ *
4
+ * Manages authentication state transitions for WebSocket connections.
5
+ * Enforces the tiered authentication model with no-downgrade rules.
6
+ *
7
+ * @module server/security/auth/state-machine
8
+ */
9
+
10
+ /** @enum {string} */
11
+ const AuthState = {
12
+ GUEST: "GUEST",
13
+ AUTHENTICATING: "AUTHENTICATING",
14
+ AUTHENTICATED: "AUTHENTICATED",
15
+ MFA_PENDING: "MFA_PENDING",
16
+ ELEVATED: "ELEVATED",
17
+ KEY_RECOVERY_PENDING: "KEY_RECOVERY_PENDING",
18
+ HIGH_SECURITY: "HIGH_SECURITY",
19
+ };
20
+
21
+ /** @enum {number} */
22
+ const AuthTier = { GUEST: 0, BASIC: 1, ELEVATED: 2, HIGH_SECURITY: 3 };
23
+
24
+ const STATE_TO_TIER = {
25
+ [AuthState.GUEST]: AuthTier.GUEST,
26
+ [AuthState.AUTHENTICATING]: AuthTier.GUEST,
27
+ [AuthState.AUTHENTICATED]: AuthTier.BASIC,
28
+ [AuthState.MFA_PENDING]: AuthTier.BASIC,
29
+ [AuthState.ELEVATED]: AuthTier.ELEVATED,
30
+ [AuthState.KEY_RECOVERY_PENDING]: AuthTier.ELEVATED,
31
+ [AuthState.HIGH_SECURITY]: AuthTier.HIGH_SECURITY,
32
+ };
33
+
34
+ const VALID_TRANSITIONS = {
35
+ [AuthState.GUEST]: [AuthState.AUTHENTICATING],
36
+ [AuthState.AUTHENTICATING]: [AuthState.GUEST, AuthState.AUTHENTICATED],
37
+ [AuthState.AUTHENTICATED]: [AuthState.MFA_PENDING, AuthState.KEY_RECOVERY_PENDING],
38
+ [AuthState.MFA_PENDING]: [AuthState.AUTHENTICATED, AuthState.ELEVATED],
39
+ [AuthState.ELEVATED]: [AuthState.KEY_RECOVERY_PENDING],
40
+ [AuthState.KEY_RECOVERY_PENDING]: [AuthState.ELEVATED, AuthState.HIGH_SECURITY],
41
+ [AuthState.HIGH_SECURITY]: [],
42
+ };
43
+
44
+ /** @enum {string} */
45
+ const AuthError = {
46
+ INVALID_TRANSITION: "INVALID_TRANSITION",
47
+ AUTH_IN_PROGRESS: "AUTH_IN_PROGRESS",
48
+ ALREADY_AUTHENTICATED: "ALREADY_AUTHENTICATED",
49
+ AUTH_TIMEOUT: "AUTH_TIMEOUT",
50
+ INVALID_PROOF: "INVALID_PROOF",
51
+ NONCE_EXPIRED: "NONCE_EXPIRED",
52
+ NONCE_REUSED: "NONCE_REUSED",
53
+ RATE_LIMITED: "RATE_LIMITED",
54
+ USER_NOT_FOUND: "USER_NOT_FOUND",
55
+ NO_DOWNGRADE: "NO_DOWNGRADE",
56
+ };
57
+
58
+ const DEFAULT_CONFIG = {
59
+ authTimeout: 60000,
60
+ maxAttempts: 5,
61
+ lockoutDuration: 300000,
62
+ nonceExpiry: 30000,
63
+ };
64
+
65
+ /**
66
+ * Create an authentication state manager for a socket
67
+ * @param {Object} [config={}] - Configuration options
68
+ * @returns {Object} Auth state manager
69
+ */
70
+ function createAuthStateMachine(config = {}) {
71
+ const cfg = { ...DEFAULT_CONFIG, ...config };
72
+ let state = AuthState.GUEST;
73
+ let principal = null;
74
+ let attempts = 0;
75
+ let lockoutUntil = 0;
76
+ let authTimeoutTimer = null;
77
+
78
+ const { createNonceManager } = require("./nonce-manager");
79
+ const nonceManager = createNonceManager({ nonceExpiry: cfg.nonceExpiry, AuthError });
80
+
81
+ /** @returns {number} */
82
+ function getTier() { return STATE_TO_TIER[state]; }
83
+
84
+ /**
85
+ * @param {string} from
86
+ * @param {string} to
87
+ * @returns {boolean}
88
+ */
89
+ function isValidTransition(from, to) {
90
+ return (VALID_TRANSITIONS[from] || []).includes(to);
91
+ }
92
+
93
+ /**
94
+ * @param {string} newState
95
+ * @returns {string}
96
+ */
97
+ function transition(newState) {
98
+ if (!isValidTransition(state, newState)) {
99
+ const err = new Error(`Invalid transition: ${state} -> ${newState}`);
100
+ err.code = AuthError.INVALID_TRANSITION;
101
+ throw err;
102
+ }
103
+ const oldTier = getTier();
104
+ const newTier = STATE_TO_TIER[newState];
105
+ if (newTier < oldTier && newState !== AuthState.GUEST) {
106
+ const err = new Error(`Cannot downgrade from tier ${oldTier} to ${newTier}`);
107
+ err.code = AuthError.NO_DOWNGRADE;
108
+ throw err;
109
+ }
110
+ const oldState = state;
111
+ state = newState;
112
+ return oldState;
113
+ }
114
+
115
+ /** @returns {boolean} */
116
+ function isLockedOut() {
117
+ if (lockoutUntil === 0) return false;
118
+ if (Date.now() >= lockoutUntil) { lockoutUntil = 0; attempts = 0; return false; }
119
+ return true;
120
+ }
121
+
122
+ /** @returns {Object} */
123
+ function recordFailedAttempt() {
124
+ attempts++;
125
+ if (attempts >= cfg.maxAttempts) lockoutUntil = Date.now() + cfg.lockoutDuration;
126
+ return {
127
+ attempts,
128
+ maxAttempts: cfg.maxAttempts,
129
+ lockedOut: isLockedOut(),
130
+ lockoutRemaining: lockoutUntil > 0 ? lockoutUntil - Date.now() : 0,
131
+ };
132
+ }
133
+
134
+ /** Reset attempt counter (called on successful auth) */
135
+ function resetAttempts() { attempts = 0; lockoutUntil = 0; }
136
+
137
+ /**
138
+ * @param {string} method
139
+ * @returns {Object}
140
+ */
141
+ function startAuth(method) {
142
+ if (isLockedOut()) {
143
+ const err = new Error("Too many authentication attempts");
144
+ err.code = AuthError.RATE_LIMITED;
145
+ err.lockoutRemaining = lockoutUntil - Date.now();
146
+ throw err;
147
+ }
148
+ if (state === AuthState.AUTHENTICATING) {
149
+ const err = new Error("Authentication already in progress");
150
+ err.code = AuthError.AUTH_IN_PROGRESS;
151
+ throw err;
152
+ }
153
+ if (getTier() >= AuthTier.BASIC) {
154
+ const err = new Error("Already authenticated");
155
+ err.code = AuthError.ALREADY_AUTHENTICATED;
156
+ throw err;
157
+ }
158
+ transition(AuthState.AUTHENTICATING);
159
+ authTimeoutTimer = setTimeout(() => {
160
+ if (state === AuthState.AUTHENTICATING) transition(AuthState.GUEST);
161
+ }, cfg.authTimeout);
162
+ return { state, method };
163
+ }
164
+
165
+ /**
166
+ * @param {Object} principalData
167
+ * @returns {Object}
168
+ */
169
+ function completeAuth(principalData) {
170
+ if (state !== AuthState.AUTHENTICATING) {
171
+ const err = new Error("Not in authenticating state");
172
+ err.code = AuthError.INVALID_TRANSITION;
173
+ throw err;
174
+ }
175
+ if (authTimeoutTimer) { clearTimeout(authTimeoutTimer); authTimeoutTimer = null; }
176
+ principal = {
177
+ userId: principalData.userId,
178
+ roles: principalData.roles || [],
179
+ permissions: principalData.permissions || {},
180
+ authenticatedAt: Date.now(),
181
+ };
182
+ transition(AuthState.AUTHENTICATED);
183
+ resetAttempts();
184
+ return { state, tier: getTier(), principal };
185
+ }
186
+
187
+ /**
188
+ * @param {string} reason
189
+ * @returns {Object}
190
+ */
191
+ function failAuth(reason) {
192
+ if (authTimeoutTimer) { clearTimeout(authTimeoutTimer); authTimeoutTimer = null; }
193
+ if (state === AuthState.AUTHENTICATING) transition(AuthState.GUEST);
194
+ return { state, reason, ...recordFailedAttempt() };
195
+ }
196
+
197
+ const { createMFAFunctions } = require("./state-machine-mfa");
198
+ const mfaFunctions = createMFAFunctions({
199
+ getState: () => ({ state }),
200
+ getTier,
201
+ transition,
202
+ generateNonce: nonceManager.generateNonce,
203
+ getPrincipal: () => principal,
204
+ setPrincipal: (p) => { principal = p; },
205
+ AuthState,
206
+ AuthTier,
207
+ AuthError,
208
+ });
209
+
210
+ /** @returns {Object} */
211
+ function getState() {
212
+ return {
213
+ state,
214
+ tier: getTier(),
215
+ principal: principal ? { ...principal } : null,
216
+ isAuthenticated: getTier() >= AuthTier.BASIC,
217
+ isElevated: getTier() >= AuthTier.ELEVATED,
218
+ isHighSecurity: getTier() >= AuthTier.HIGH_SECURITY,
219
+ };
220
+ }
221
+
222
+ /** Clean up resources (call on socket close) */
223
+ function cleanup() {
224
+ if (authTimeoutTimer) { clearTimeout(authTimeoutTimer); authTimeoutTimer = null; }
225
+ nonceManager.clearPendingNonces();
226
+ }
227
+
228
+ return {
229
+ getState,
230
+ getTier,
231
+ startAuth,
232
+ completeAuth,
233
+ failAuth,
234
+ startMFA: mfaFunctions.startMFA,
235
+ completeMFA: mfaFunctions.completeMFA,
236
+ // Key recovery (2-of-3 Tier 3) functions
237
+ startKeyRecovery: mfaFunctions.startKeyRecovery,
238
+ completeKeyRecovery: mfaFunctions.completeKeyRecovery,
239
+ cancelKeyRecovery: mfaFunctions.cancelKeyRecovery,
240
+ getKeyRecoveryStatus: mfaFunctions.getKeyRecoveryStatus,
241
+ generateNonce: nonceManager.generateNonce,
242
+ consumeNonce: nonceManager.consumeNonce,
243
+ isLockedOut,
244
+ cleanup,
245
+ AuthState,
246
+ AuthTier,
247
+ AuthError,
248
+ };
249
+ }
250
+
251
+ module.exports = {
252
+ createAuthStateMachine,
253
+ AuthState,
254
+ AuthTier,
255
+ AuthError,
256
+ DEFAULT_CONFIG,
257
+ };