tlc-claude-code 1.3.0 → 1.4.1
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/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA Handler
|
|
3
|
+
* Multi-factor authentication support with TOTP and backup codes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// Base32 alphabet for encoding secrets
|
|
9
|
+
const BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
10
|
+
|
|
11
|
+
// TOTP configuration (RFC 6238)
|
|
12
|
+
const TOTP_CONFIG = {
|
|
13
|
+
algorithm: 'sha1',
|
|
14
|
+
digits: 6,
|
|
15
|
+
period: 30, // 30-second time step
|
|
16
|
+
window: 1, // Allow +-1 time step for clock drift
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encode bytes to base32
|
|
21
|
+
* @param {Buffer} buffer - Bytes to encode
|
|
22
|
+
* @returns {string} Base32 encoded string
|
|
23
|
+
*/
|
|
24
|
+
function base32Encode(buffer) {
|
|
25
|
+
let bits = '';
|
|
26
|
+
for (const byte of buffer) {
|
|
27
|
+
bits += byte.toString(2).padStart(8, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let result = '';
|
|
31
|
+
for (let i = 0; i < bits.length; i += 5) {
|
|
32
|
+
const chunk = bits.slice(i, i + 5).padEnd(5, '0');
|
|
33
|
+
result += BASE32_CHARS[parseInt(chunk, 2)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decode base32 string to bytes
|
|
41
|
+
* @param {string} str - Base32 encoded string
|
|
42
|
+
* @returns {Buffer} Decoded bytes
|
|
43
|
+
*/
|
|
44
|
+
function base32Decode(str) {
|
|
45
|
+
let bits = '';
|
|
46
|
+
for (const char of str.toUpperCase()) {
|
|
47
|
+
const val = BASE32_CHARS.indexOf(char);
|
|
48
|
+
if (val >= 0) {
|
|
49
|
+
bits += val.toString(2).padStart(5, '0');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bytes = [];
|
|
54
|
+
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
|
55
|
+
bytes.push(parseInt(bits.slice(i, i + 8), 2));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Buffer.from(bytes);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate TOTP code for given secret and counter
|
|
63
|
+
* @param {string} secret - Base32 encoded secret
|
|
64
|
+
* @param {number} counter - Time counter
|
|
65
|
+
* @returns {string} 6-digit TOTP code
|
|
66
|
+
*/
|
|
67
|
+
function generateTotp(secret, counter) {
|
|
68
|
+
const key = base32Decode(secret);
|
|
69
|
+
|
|
70
|
+
// Convert counter to 8-byte big-endian buffer
|
|
71
|
+
const counterBuffer = Buffer.alloc(8);
|
|
72
|
+
counterBuffer.writeBigInt64BE(BigInt(counter));
|
|
73
|
+
|
|
74
|
+
// Generate HMAC-SHA1
|
|
75
|
+
const hmac = crypto.createHmac(TOTP_CONFIG.algorithm, key);
|
|
76
|
+
hmac.update(counterBuffer);
|
|
77
|
+
const hash = hmac.digest();
|
|
78
|
+
|
|
79
|
+
// Dynamic truncation (RFC 4226)
|
|
80
|
+
const offset = hash[hash.length - 1] & 0x0f;
|
|
81
|
+
const code = (
|
|
82
|
+
((hash[offset] & 0x7f) << 24) |
|
|
83
|
+
((hash[offset + 1] & 0xff) << 16) |
|
|
84
|
+
((hash[offset + 2] & 0xff) << 8) |
|
|
85
|
+
(hash[offset + 3] & 0xff)
|
|
86
|
+
) % Math.pow(10, TOTP_CONFIG.digits);
|
|
87
|
+
|
|
88
|
+
return code.toString().padStart(TOTP_CONFIG.digits, '0');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get current time counter
|
|
93
|
+
* @param {number} offset - Time step offset
|
|
94
|
+
* @returns {number} Time counter
|
|
95
|
+
*/
|
|
96
|
+
function getTimeCounter(offset = 0) {
|
|
97
|
+
return Math.floor(Date.now() / 1000 / TOTP_CONFIG.period) + offset;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate TOTP secret for a user
|
|
102
|
+
* @param {string} email - User's email for QR code URL
|
|
103
|
+
* @returns {Object} Secret and QR code URL
|
|
104
|
+
*/
|
|
105
|
+
function generateSecret(email) {
|
|
106
|
+
// Generate 20 random bytes for the secret (160 bits as recommended)
|
|
107
|
+
const secretBytes = crypto.randomBytes(20);
|
|
108
|
+
const secret = base32Encode(secretBytes);
|
|
109
|
+
|
|
110
|
+
// Build otpauth:// URL for QR code
|
|
111
|
+
const encodedEmail = encodeURIComponent(email);
|
|
112
|
+
const qrCodeUrl = `otpauth://totp/TLC:${encodedEmail}?secret=${secret}&issuer=TLC`;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
secret,
|
|
116
|
+
qrCodeUrl,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate TOTP code
|
|
122
|
+
* @param {string} secret - Base32 encoded secret
|
|
123
|
+
* @param {string} code - 6-digit code to validate
|
|
124
|
+
* @returns {boolean} Whether code is valid
|
|
125
|
+
*/
|
|
126
|
+
function validateCode(secret, code) {
|
|
127
|
+
if (!secret || !code) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate code format
|
|
132
|
+
if (typeof code !== 'string' || code.length !== TOTP_CONFIG.digits) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!/^\d+$/.test(code)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check code against current time and allowed window
|
|
141
|
+
const currentCounter = getTimeCounter();
|
|
142
|
+
|
|
143
|
+
for (let offset = -TOTP_CONFIG.window; offset <= TOTP_CONFIG.window; offset++) {
|
|
144
|
+
const expectedCode = generateTotp(secret, currentCounter + offset);
|
|
145
|
+
if (timingSafeEqual(code, expectedCode)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Timing-safe string comparison
|
|
155
|
+
* @param {string} a - First string
|
|
156
|
+
* @param {string} b - Second string
|
|
157
|
+
* @returns {boolean} Whether strings are equal
|
|
158
|
+
*/
|
|
159
|
+
function timingSafeEqual(a, b) {
|
|
160
|
+
if (a.length !== b.length) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate random alphanumeric string
|
|
173
|
+
* @param {number} length - String length
|
|
174
|
+
* @returns {string} Random alphanumeric string (uppercase)
|
|
175
|
+
*/
|
|
176
|
+
function generateAlphanumeric(length) {
|
|
177
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
178
|
+
let result = '';
|
|
179
|
+
|
|
180
|
+
const randomBytes = crypto.randomBytes(length);
|
|
181
|
+
for (let i = 0; i < length; i++) {
|
|
182
|
+
result += chars[randomBytes[i] % chars.length];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Hash a backup code for storage
|
|
190
|
+
* @param {string} code - Plain backup code
|
|
191
|
+
* @returns {string} Hashed code
|
|
192
|
+
*/
|
|
193
|
+
function hashBackupCode(code) {
|
|
194
|
+
return crypto.createHash('sha256').update(code.toUpperCase()).digest('hex');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate backup codes
|
|
199
|
+
* @returns {Object} Plain codes (for user) and hashed codes (for storage)
|
|
200
|
+
*/
|
|
201
|
+
function generateBackupCodes() {
|
|
202
|
+
const codes = [];
|
|
203
|
+
const hashedCodes = [];
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < 10; i++) {
|
|
206
|
+
const code = generateAlphanumeric(8);
|
|
207
|
+
codes.push(code);
|
|
208
|
+
hashedCodes.push(hashBackupCode(code));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
codes,
|
|
213
|
+
hashedCodes,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate a backup code
|
|
219
|
+
* @param {string} code - Backup code to validate
|
|
220
|
+
* @param {string[]} hashedCodes - Array of hashed backup codes
|
|
221
|
+
* @param {Set<number>} usedCodes - Set of used code indices
|
|
222
|
+
* @returns {Object} Validation result with valid flag and index
|
|
223
|
+
*/
|
|
224
|
+
function validateBackupCode(code, hashedCodes, usedCodes) {
|
|
225
|
+
if (!code || !hashedCodes) {
|
|
226
|
+
return { valid: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const normalizedCode = code.toUpperCase();
|
|
230
|
+
const codeHash = hashBackupCode(normalizedCode);
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < hashedCodes.length; i++) {
|
|
233
|
+
// Skip used codes
|
|
234
|
+
if (usedCodes && usedCodes.has(i)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (timingSafeEqual(codeHash, hashedCodes[i])) {
|
|
239
|
+
return { valid: true, index: i };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { valid: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if MFA is required for user based on user settings and policy
|
|
248
|
+
* @param {Object} user - User object
|
|
249
|
+
* @param {Object} policy - MFA policy configuration
|
|
250
|
+
* @returns {boolean} Whether MFA is required
|
|
251
|
+
*/
|
|
252
|
+
function isMfaRequired(user, policy) {
|
|
253
|
+
if (!user) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// User has MFA enabled
|
|
258
|
+
if (user.mfaEnabled) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check policy
|
|
263
|
+
if (!policy) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Policy requires MFA for all users
|
|
268
|
+
if (policy.required) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Policy requires MFA for specific roles
|
|
273
|
+
if (policy.requiredRoles && user.role) {
|
|
274
|
+
return policy.requiredRoles.includes(user.role);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Enable MFA requirement for user
|
|
282
|
+
* @param {Object} user - User object
|
|
283
|
+
* @returns {Object} Updated user object
|
|
284
|
+
*/
|
|
285
|
+
function enforceMfa(user) {
|
|
286
|
+
if (!user) {
|
|
287
|
+
throw new Error('User is required');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
...user,
|
|
292
|
+
mfaEnabled: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Disable MFA for user
|
|
298
|
+
* @param {Object} user - User object
|
|
299
|
+
* @returns {Object} Updated user object with MFA data removed
|
|
300
|
+
*/
|
|
301
|
+
function disableMfa(user) {
|
|
302
|
+
if (!user) {
|
|
303
|
+
throw new Error('User is required');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { mfaSecret, backupCodes, usedBackupCodes, ...rest } = user;
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
...rest,
|
|
310
|
+
mfaEnabled: false,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create in-memory MFA store
|
|
316
|
+
* @returns {Object} MFA store
|
|
317
|
+
*/
|
|
318
|
+
function createMfaStore() {
|
|
319
|
+
const mfaData = new Map();
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
/**
|
|
323
|
+
* Set up MFA for a user
|
|
324
|
+
* @param {string} userId - User ID
|
|
325
|
+
* @param {string} email - User email
|
|
326
|
+
* @returns {Object} Setup data including secret, QR URL, and backup codes
|
|
327
|
+
*/
|
|
328
|
+
async setupMfa(userId, email) {
|
|
329
|
+
const { secret, qrCodeUrl } = generateSecret(email);
|
|
330
|
+
const { codes, hashedCodes } = generateBackupCodes();
|
|
331
|
+
|
|
332
|
+
mfaData.set(userId, {
|
|
333
|
+
secret,
|
|
334
|
+
hashedCodes,
|
|
335
|
+
usedCodes: new Set(),
|
|
336
|
+
enabled: true,
|
|
337
|
+
setupAt: new Date().toISOString(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
secret,
|
|
342
|
+
qrCodeUrl,
|
|
343
|
+
backupCodes: codes,
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Verify TOTP code for user
|
|
349
|
+
* @param {string} userId - User ID
|
|
350
|
+
* @param {string} code - TOTP code
|
|
351
|
+
* @returns {Object} Verification result
|
|
352
|
+
*/
|
|
353
|
+
async verifyMfa(userId, code) {
|
|
354
|
+
const data = mfaData.get(userId);
|
|
355
|
+
|
|
356
|
+
if (!data || !data.enabled) {
|
|
357
|
+
return { valid: false, error: 'MFA not enabled' };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const valid = validateCode(data.secret, code);
|
|
361
|
+
|
|
362
|
+
return { valid };
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Verify backup code for user (marks as used if valid)
|
|
367
|
+
* @param {string} userId - User ID
|
|
368
|
+
* @param {string} code - Backup code
|
|
369
|
+
* @returns {Object} Verification result
|
|
370
|
+
*/
|
|
371
|
+
async verifyBackupCode(userId, code) {
|
|
372
|
+
const data = mfaData.get(userId);
|
|
373
|
+
|
|
374
|
+
if (!data || !data.enabled) {
|
|
375
|
+
return { valid: false, error: 'MFA not enabled' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = validateBackupCode(code, data.hashedCodes, data.usedCodes);
|
|
379
|
+
|
|
380
|
+
if (result.valid) {
|
|
381
|
+
// Mark code as used
|
|
382
|
+
data.usedCodes.add(result.index);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { valid: result.valid };
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Regenerate backup codes for user
|
|
390
|
+
* @param {string} userId - User ID
|
|
391
|
+
* @returns {string[]} New backup codes
|
|
392
|
+
*/
|
|
393
|
+
async regenerateBackupCodes(userId) {
|
|
394
|
+
const data = mfaData.get(userId);
|
|
395
|
+
|
|
396
|
+
if (!data) {
|
|
397
|
+
throw new Error('MFA not set up for user');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const { codes, hashedCodes } = generateBackupCodes();
|
|
401
|
+
|
|
402
|
+
data.hashedCodes = hashedCodes;
|
|
403
|
+
data.usedCodes = new Set();
|
|
404
|
+
|
|
405
|
+
return codes;
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get MFA status for user
|
|
410
|
+
* @param {string} userId - User ID
|
|
411
|
+
* @returns {Object} MFA status
|
|
412
|
+
*/
|
|
413
|
+
async getMfaStatus(userId) {
|
|
414
|
+
const data = mfaData.get(userId);
|
|
415
|
+
|
|
416
|
+
if (!data) {
|
|
417
|
+
return { enabled: false };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const backupCodesRemaining = data.hashedCodes.length - data.usedCodes.size;
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
enabled: data.enabled,
|
|
424
|
+
backupCodesRemaining,
|
|
425
|
+
setupAt: data.setupAt,
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Remove MFA from user
|
|
431
|
+
* @param {string} userId - User ID
|
|
432
|
+
*/
|
|
433
|
+
async removeMfa(userId) {
|
|
434
|
+
mfaData.delete(userId);
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
module.exports = {
|
|
440
|
+
generateSecret,
|
|
441
|
+
validateCode,
|
|
442
|
+
generateBackupCodes,
|
|
443
|
+
validateBackupCode,
|
|
444
|
+
isMfaRequired,
|
|
445
|
+
enforceMfa,
|
|
446
|
+
disableMfa,
|
|
447
|
+
createMfaStore,
|
|
448
|
+
// Export for testing
|
|
449
|
+
base32Encode,
|
|
450
|
+
base32Decode,
|
|
451
|
+
generateTotp,
|
|
452
|
+
};
|