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.
- package/README.md +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- 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
|
+
};
|