@eaccess/auth 0.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/dist/index.cjs ADDED
@@ -0,0 +1,2632 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ActivityLogger: () => ActivityLogger,
34
+ AuthActivityAction: () => AuthActivityAction,
35
+ AuthError: () => AuthError,
36
+ AuthRole: () => AuthRole,
37
+ AuthStatus: () => AuthStatus,
38
+ AzureProvider: () => AzureProvider,
39
+ BaseOAuthProvider: () => BaseOAuthProvider,
40
+ ConfirmationExpiredError: () => ConfirmationExpiredError,
41
+ ConfirmationNotFoundError: () => ConfirmationNotFoundError,
42
+ EmailNotVerifiedError: () => EmailNotVerifiedError,
43
+ EmailTakenError: () => EmailTakenError,
44
+ GitHubProvider: () => GitHubProvider,
45
+ GoogleProvider: () => GoogleProvider,
46
+ InvalidBackupCodeError: () => InvalidBackupCodeError,
47
+ InvalidEmailError: () => InvalidEmailError,
48
+ InvalidPasswordError: () => InvalidPasswordError,
49
+ InvalidTokenError: () => InvalidTokenError,
50
+ InvalidTwoFactorCodeError: () => InvalidTwoFactorCodeError,
51
+ OtpProvider: () => OtpProvider,
52
+ ResetDisabledError: () => ResetDisabledError,
53
+ ResetExpiredError: () => ResetExpiredError,
54
+ ResetNotFoundError: () => ResetNotFoundError,
55
+ SecondFactorRequiredError: () => SecondFactorRequiredError,
56
+ TooManyResetsError: () => TooManyResetsError,
57
+ TotpProvider: () => TotpProvider,
58
+ TwoFactorAlreadyEnabledError: () => TwoFactorAlreadyEnabledError,
59
+ TwoFactorExpiredError: () => TwoFactorExpiredError,
60
+ TwoFactorManager: () => TwoFactorManager,
61
+ TwoFactorMechanism: () => TwoFactorMechanism,
62
+ TwoFactorNotSetupError: () => TwoFactorNotSetupError,
63
+ TwoFactorSetupIncompleteError: () => TwoFactorSetupIncompleteError,
64
+ UserInactiveError: () => UserInactiveError,
65
+ UserNotFoundError: () => UserNotFoundError,
66
+ UserNotLoggedInError: () => UserNotLoggedInError,
67
+ cleanupExpiredTokens: () => cleanupExpiredTokens,
68
+ createAuthMiddleware: () => createAuthMiddleware,
69
+ createAuthTables: () => createAuthTables,
70
+ dropAuthTables: () => dropAuthTables,
71
+ getAuthTableStats: () => getAuthTableStats,
72
+ isValidEmail: () => isValidEmail,
73
+ validateEmail: () => validateEmail
74
+ });
75
+ module.exports = __toCommonJS(index_exports);
76
+
77
+ // src/auth-admin-manager.ts
78
+ var import_hash = require("@prsm/hash");
79
+ var import_ms = __toESM(require("@prsm/ms"), 1);
80
+
81
+ // src/types.ts
82
+ var import_express_session = require("express-session");
83
+ var TwoFactorMechanism = /* @__PURE__ */ ((TwoFactorMechanism2) => {
84
+ TwoFactorMechanism2[TwoFactorMechanism2["TOTP"] = 1] = "TOTP";
85
+ TwoFactorMechanism2[TwoFactorMechanism2["EMAIL"] = 2] = "EMAIL";
86
+ TwoFactorMechanism2[TwoFactorMechanism2["SMS"] = 3] = "SMS";
87
+ return TwoFactorMechanism2;
88
+ })(TwoFactorMechanism || {});
89
+ var AuthStatus = {
90
+ Normal: 0,
91
+ Archived: 1,
92
+ Banned: 2,
93
+ Locked: 3,
94
+ PendingReview: 4,
95
+ Suspended: 5
96
+ };
97
+ var AuthRole = {
98
+ Admin: 1,
99
+ Author: 2,
100
+ Collaborator: 4,
101
+ Consultant: 8,
102
+ Consumer: 16,
103
+ Contributor: 32,
104
+ Coordinator: 64,
105
+ Creator: 128,
106
+ Developer: 256,
107
+ Director: 512,
108
+ Editor: 1024,
109
+ Employee: 2048,
110
+ Maintainer: 4096,
111
+ Manager: 8192,
112
+ Moderator: 16384,
113
+ Publisher: 32768,
114
+ Reviewer: 65536,
115
+ Subscriber: 131072,
116
+ SuperAdmin: 262144,
117
+ SuperEditor: 524288,
118
+ SuperModerator: 1048576,
119
+ Translator: 2097152
120
+ };
121
+ var AuthActivityAction = {
122
+ Login: "login",
123
+ Logout: "logout",
124
+ FailedLogin: "failed_login",
125
+ Register: "register",
126
+ EmailConfirmed: "email_confirmed",
127
+ PasswordResetRequested: "password_reset_requested",
128
+ PasswordResetCompleted: "password_reset_completed",
129
+ PasswordChanged: "password_changed",
130
+ EmailChanged: "email_changed",
131
+ RoleChanged: "role_changed",
132
+ StatusChanged: "status_changed",
133
+ ForceLogout: "force_logout",
134
+ OAuthConnected: "oauth_connected",
135
+ RememberTokenCreated: "remember_token_created",
136
+ TwoFactorSetup: "two_factor_setup",
137
+ TwoFactorVerified: "two_factor_verified",
138
+ TwoFactorFailed: "two_factor_failed",
139
+ TwoFactorDisabled: "two_factor_disabled",
140
+ BackupCodeUsed: "backup_code_used"
141
+ };
142
+
143
+ // src/queries.ts
144
+ var AuthQueries = class {
145
+ constructor(config) {
146
+ this.db = config.db;
147
+ this.tablePrefix = config.tablePrefix || "user_";
148
+ }
149
+ get accountsTable() {
150
+ return `${this.tablePrefix}accounts`;
151
+ }
152
+ get confirmationsTable() {
153
+ return `${this.tablePrefix}confirmations`;
154
+ }
155
+ get remembersTable() {
156
+ return `${this.tablePrefix}remembers`;
157
+ }
158
+ get resetsTable() {
159
+ return `${this.tablePrefix}resets`;
160
+ }
161
+ get providersTable() {
162
+ return `${this.tablePrefix}providers`;
163
+ }
164
+ get twoFactorMethodsTable() {
165
+ return `${this.tablePrefix}2fa_methods`;
166
+ }
167
+ get twoFactorTokensTable() {
168
+ return `${this.tablePrefix}2fa_tokens`;
169
+ }
170
+ async findAccountById(id) {
171
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE id = $1`;
172
+ const result = await this.db.query(sql, [id]);
173
+ return result.rows[0] || null;
174
+ }
175
+ async findAccountByUserId(userId) {
176
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE user_id = $1`;
177
+ const result = await this.db.query(sql, [userId]);
178
+ return result.rows[0] || null;
179
+ }
180
+ async findAccountByEmail(email) {
181
+ const sql = `SELECT * FROM ${this.accountsTable} WHERE email = $1`;
182
+ const result = await this.db.query(sql, [email]);
183
+ return result.rows[0] || null;
184
+ }
185
+ async createAccount(data) {
186
+ const sql = `
187
+ INSERT INTO ${this.accountsTable} (
188
+ user_id, email, password, verified, status, rolemask,
189
+ force_logout, resettable, registered
190
+ )
191
+ VALUES ($1, $2, $3, $4, $5, $6, 0, true, NOW())
192
+ RETURNING *
193
+ `;
194
+ const result = await this.db.query(sql, [data.userId, data.email, data.password, data.verified, data.status, data.rolemask]);
195
+ return result.rows[0];
196
+ }
197
+ async updateAccount(id, updates) {
198
+ const fields = [];
199
+ const values = [];
200
+ let paramIndex = 1;
201
+ for (const [key, value] of Object.entries(updates)) {
202
+ if (key === "id") continue;
203
+ fields.push(`${key} = $${paramIndex++}`);
204
+ values.push(value);
205
+ }
206
+ if (fields.length === 0) return;
207
+ values.push(id);
208
+ const sql = `UPDATE ${this.accountsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`;
209
+ await this.db.query(sql, values);
210
+ }
211
+ async updateAccountLastLogin(id) {
212
+ const sql = `UPDATE ${this.accountsTable} SET last_login = NOW() WHERE id = $1`;
213
+ await this.db.query(sql, [id]);
214
+ }
215
+ async incrementForceLogout(id) {
216
+ const sql = `UPDATE ${this.accountsTable} SET force_logout = force_logout + 1 WHERE id = $1`;
217
+ await this.db.query(sql, [id]);
218
+ }
219
+ async deleteAccount(id) {
220
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [id]);
221
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [id]);
222
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [id]);
223
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [id]);
224
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [id]);
225
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [id]);
226
+ await this.db.query(`DELETE FROM ${this.accountsTable} WHERE id = $1`, [id]);
227
+ }
228
+ async createConfirmation(data) {
229
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [data.accountId]);
230
+ const sql = `
231
+ INSERT INTO ${this.confirmationsTable} (account_id, token, email, expires)
232
+ VALUES ($1, $2, $3, $4)
233
+ `;
234
+ await this.db.query(sql, [data.accountId, data.token, data.email, data.expires]);
235
+ }
236
+ async findConfirmation(token) {
237
+ const sql = `SELECT * FROM ${this.confirmationsTable} WHERE token = $1`;
238
+ const result = await this.db.query(sql, [token]);
239
+ return result.rows[0] || null;
240
+ }
241
+ async findLatestConfirmationForAccount(accountId) {
242
+ const sql = `
243
+ SELECT * FROM ${this.confirmationsTable}
244
+ WHERE account_id = $1
245
+ ORDER BY expires DESC
246
+ LIMIT 1
247
+ `;
248
+ const result = await this.db.query(sql, [accountId]);
249
+ return result.rows[0] || null;
250
+ }
251
+ async deleteConfirmation(token) {
252
+ await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE token = $1`, [token]);
253
+ }
254
+ async createRememberToken(data) {
255
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [data.accountId]);
256
+ const sql = `
257
+ INSERT INTO ${this.remembersTable} (account_id, token, expires)
258
+ VALUES ($1, $2, $3)
259
+ `;
260
+ await this.db.query(sql, [data.accountId, data.token, data.expires]);
261
+ }
262
+ async findRememberToken(token) {
263
+ const sql = `SELECT * FROM ${this.remembersTable} WHERE token = $1`;
264
+ const result = await this.db.query(sql, [token]);
265
+ return result.rows[0] || null;
266
+ }
267
+ async deleteRememberToken(token) {
268
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE token = $1`, [token]);
269
+ }
270
+ async deleteRememberTokensForAccount(accountId) {
271
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [accountId]);
272
+ }
273
+ async deleteExpiredRememberTokensForAccount(accountId) {
274
+ await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1 AND expires <= NOW()`, [accountId]);
275
+ }
276
+ async createResetToken(data) {
277
+ const sql = `
278
+ INSERT INTO ${this.resetsTable} (account_id, token, expires)
279
+ VALUES ($1, $2, $3)
280
+ `;
281
+ await this.db.query(sql, [data.accountId, data.token, data.expires]);
282
+ }
283
+ async findResetToken(token) {
284
+ const sql = `
285
+ SELECT * FROM ${this.resetsTable}
286
+ WHERE token = $1
287
+ ORDER BY expires DESC
288
+ LIMIT 1
289
+ `;
290
+ const result = await this.db.query(sql, [token]);
291
+ return result.rows[0] || null;
292
+ }
293
+ async countActiveResetTokensForAccount(accountId) {
294
+ const sql = `
295
+ SELECT COUNT(*) as count FROM ${this.resetsTable}
296
+ WHERE account_id = $1 AND expires >= NOW()
297
+ `;
298
+ const result = await this.db.query(sql, [accountId]);
299
+ return parseInt(result.rows[0]?.count || "0");
300
+ }
301
+ async deleteResetToken(token) {
302
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE token = $1`, [token]);
303
+ }
304
+ async deleteResetTokensForAccount(accountId) {
305
+ await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [accountId]);
306
+ }
307
+ async createProvider(data) {
308
+ const sql = `
309
+ INSERT INTO ${this.providersTable} (
310
+ account_id, provider, provider_id, provider_email,
311
+ provider_username, provider_name, provider_avatar
312
+ )
313
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
314
+ RETURNING *
315
+ `;
316
+ const result = await this.db.query(sql, [data.accountId, data.provider, data.providerId, data.providerEmail, data.providerUsername, data.providerName, data.providerAvatar]);
317
+ return result.rows[0];
318
+ }
319
+ async findProviderByProviderIdAndType(providerId, provider) {
320
+ const sql = `SELECT * FROM ${this.providersTable} WHERE provider_id = $1 AND provider = $2`;
321
+ const result = await this.db.query(sql, [providerId, provider]);
322
+ return result.rows[0] || null;
323
+ }
324
+ async findProvidersByAccountId(accountId) {
325
+ const sql = `SELECT * FROM ${this.providersTable} WHERE account_id = $1 ORDER BY created_at DESC`;
326
+ const result = await this.db.query(sql, [accountId]);
327
+ return result.rows;
328
+ }
329
+ async deleteProvider(id) {
330
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE id = $1`, [id]);
331
+ }
332
+ async deleteProvidersByAccountId(accountId) {
333
+ await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [accountId]);
334
+ }
335
+ // two-factor authentication methods
336
+ async findTwoFactorMethodsByAccountId(accountId) {
337
+ const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 ORDER BY created_at DESC`;
338
+ const result = await this.db.query(sql, [accountId]);
339
+ return result.rows;
340
+ }
341
+ async findTwoFactorMethodByAccountAndMechanism(accountId, mechanism) {
342
+ const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 AND mechanism = $2`;
343
+ const result = await this.db.query(sql, [accountId, mechanism]);
344
+ return result.rows[0] || null;
345
+ }
346
+ async createTwoFactorMethod(data) {
347
+ const sql = `
348
+ INSERT INTO ${this.twoFactorMethodsTable} (
349
+ account_id, mechanism, secret, backup_codes, verified
350
+ )
351
+ VALUES ($1, $2, $3, $4, $5)
352
+ RETURNING *
353
+ `;
354
+ const result = await this.db.query(sql, [data.accountId, data.mechanism, data.secret || null, data.backupCodes || null, data.verified || false]);
355
+ return result.rows[0];
356
+ }
357
+ async updateTwoFactorMethod(id, updates) {
358
+ const fields = [];
359
+ const values = [];
360
+ let paramIndex = 1;
361
+ for (const [key, value] of Object.entries(updates)) {
362
+ if (key === "id") continue;
363
+ fields.push(`${key} = $${paramIndex++}`);
364
+ values.push(value);
365
+ }
366
+ if (fields.length === 0) return;
367
+ values.push(id);
368
+ const sql = `UPDATE ${this.twoFactorMethodsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`;
369
+ await this.db.query(sql, values);
370
+ }
371
+ async deleteTwoFactorMethod(id) {
372
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE id = $1`, [id]);
373
+ }
374
+ async deleteTwoFactorMethodsByAccountId(accountId) {
375
+ await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [accountId]);
376
+ }
377
+ // two-factor authentication tokens
378
+ async createTwoFactorToken(data) {
379
+ const sql = `
380
+ INSERT INTO ${this.twoFactorTokensTable} (
381
+ account_id, mechanism, selector, token_hash, expires_at
382
+ )
383
+ VALUES ($1, $2, $3, $4, $5)
384
+ RETURNING *
385
+ `;
386
+ const result = await this.db.query(sql, [data.accountId, data.mechanism, data.selector, data.tokenHash, data.expiresAt]);
387
+ return result.rows[0];
388
+ }
389
+ async findTwoFactorTokenBySelector(selector) {
390
+ const sql = `SELECT * FROM ${this.twoFactorTokensTable} WHERE selector = $1 AND expires_at > NOW()`;
391
+ const result = await this.db.query(sql, [selector]);
392
+ return result.rows[0] || null;
393
+ }
394
+ async deleteTwoFactorToken(id) {
395
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE id = $1`, [id]);
396
+ }
397
+ async deleteTwoFactorTokensByAccountId(accountId) {
398
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [accountId]);
399
+ }
400
+ async deleteTwoFactorTokensByAccountAndMechanism(accountId, mechanism) {
401
+ await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1 AND mechanism = $2`, [accountId, mechanism]);
402
+ }
403
+ };
404
+
405
+ // src/errors.ts
406
+ var AuthError = class extends Error {
407
+ constructor(message) {
408
+ super(message);
409
+ this.name = this.constructor.name;
410
+ }
411
+ };
412
+ var ConfirmationExpiredError = class extends AuthError {
413
+ constructor() {
414
+ super("Confirmation token has expired");
415
+ }
416
+ };
417
+ var ConfirmationNotFoundError = class extends AuthError {
418
+ constructor() {
419
+ super("Confirmation token not found");
420
+ }
421
+ };
422
+ var EmailNotVerifiedError = class extends AuthError {
423
+ constructor() {
424
+ super("Email address has not been verified");
425
+ }
426
+ };
427
+ var EmailTakenError = class extends AuthError {
428
+ constructor() {
429
+ super("Email address is already in use");
430
+ }
431
+ };
432
+ var InvalidEmailError = class extends AuthError {
433
+ constructor() {
434
+ super("Invalid email address format");
435
+ }
436
+ };
437
+ var InvalidPasswordError = class extends AuthError {
438
+ constructor() {
439
+ super("Invalid password");
440
+ }
441
+ };
442
+ var InvalidTokenError = class extends AuthError {
443
+ constructor() {
444
+ super("Invalid token");
445
+ }
446
+ };
447
+ var ResetDisabledError = class extends AuthError {
448
+ constructor() {
449
+ super("Password reset is disabled for this account");
450
+ }
451
+ };
452
+ var ResetExpiredError = class extends AuthError {
453
+ constructor() {
454
+ super("Password reset token has expired");
455
+ }
456
+ };
457
+ var ResetNotFoundError = class extends AuthError {
458
+ constructor() {
459
+ super("Password reset token not found");
460
+ }
461
+ };
462
+ var TooManyResetsError = class extends AuthError {
463
+ constructor() {
464
+ super("Too many password reset requests");
465
+ }
466
+ };
467
+ var UserInactiveError = class extends AuthError {
468
+ constructor() {
469
+ super("User account is inactive");
470
+ }
471
+ };
472
+ var UserNotFoundError = class extends AuthError {
473
+ constructor() {
474
+ super("User not found");
475
+ }
476
+ };
477
+ var UserNotLoggedInError = class extends AuthError {
478
+ constructor() {
479
+ super("User is not logged in");
480
+ }
481
+ };
482
+ var SecondFactorRequiredError = class extends AuthError {
483
+ constructor(availableMethods) {
484
+ super("Second factor authentication required");
485
+ this.availableMethods = availableMethods;
486
+ }
487
+ };
488
+ var InvalidTwoFactorCodeError = class extends AuthError {
489
+ constructor() {
490
+ super("Invalid two-factor authentication code");
491
+ }
492
+ };
493
+ var TwoFactorExpiredError = class extends AuthError {
494
+ constructor() {
495
+ super("Two-factor authentication session has expired");
496
+ }
497
+ };
498
+ var TwoFactorNotSetupError = class extends AuthError {
499
+ constructor() {
500
+ super("Two-factor authentication is not set up for this account");
501
+ }
502
+ };
503
+ var TwoFactorAlreadyEnabledError = class extends AuthError {
504
+ constructor() {
505
+ super("Two-factor authentication is already enabled for this mechanism");
506
+ }
507
+ };
508
+ var InvalidBackupCodeError = class extends AuthError {
509
+ constructor() {
510
+ super("Invalid backup code");
511
+ }
512
+ };
513
+ var TwoFactorSetupIncompleteError = class extends AuthError {
514
+ constructor() {
515
+ super("Two-factor authentication setup is not complete");
516
+ }
517
+ };
518
+
519
+ // src/util.ts
520
+ var isValidEmail = (email) => {
521
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
522
+ return emailRegex.test(email);
523
+ };
524
+ var validateEmail = (email) => {
525
+ if (typeof email !== "string") {
526
+ throw new InvalidEmailError();
527
+ }
528
+ if (!email.trim()) {
529
+ throw new InvalidEmailError();
530
+ }
531
+ if (!isValidEmail(email)) {
532
+ throw new InvalidEmailError();
533
+ }
534
+ };
535
+ var createMapFromEnum = (enumObj) => Object.fromEntries(Object.entries(enumObj).map(([key, value]) => [value, key]));
536
+
537
+ // src/auth-admin-manager.ts
538
+ var AuthAdminManager = class {
539
+ constructor(req, res, config, auth) {
540
+ this.req = req;
541
+ this.res = res;
542
+ this.config = config;
543
+ this.queries = new AuthQueries(config);
544
+ this.auth = auth;
545
+ }
546
+ validatePassword(password) {
547
+ const minLength = this.config.minPasswordLength || 8;
548
+ const maxLength = this.config.maxPasswordLength || 64;
549
+ if (typeof password !== "string") {
550
+ throw new InvalidPasswordError();
551
+ }
552
+ if (password.length < minLength) {
553
+ throw new InvalidPasswordError();
554
+ }
555
+ if (password.length > maxLength) {
556
+ throw new InvalidPasswordError();
557
+ }
558
+ }
559
+ generateAutoUserId() {
560
+ return crypto.randomUUID();
561
+ }
562
+ async findAccountByIdentifier(identifier) {
563
+ if (identifier.accountId !== void 0) {
564
+ return await this.queries.findAccountById(identifier.accountId);
565
+ } else if (identifier.email !== void 0) {
566
+ return await this.queries.findAccountByEmail(identifier.email);
567
+ } else if (identifier.userId !== void 0) {
568
+ return await this.queries.findAccountByUserId(identifier.userId);
569
+ }
570
+ return null;
571
+ }
572
+ async createConfirmationToken(account, email, callback) {
573
+ const token = import_hash.hash.encode(email);
574
+ const expires = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 7);
575
+ await this.queries.createConfirmation({
576
+ accountId: account.id,
577
+ token,
578
+ email,
579
+ expires
580
+ });
581
+ if (callback) {
582
+ callback(token);
583
+ }
584
+ }
585
+ /**
586
+ * Create a new user account (admin function).
587
+ *
588
+ * @param credentials - Email and password for new account
589
+ * @param userId - Optional user ID to link this auth account to. If not provided, a UUID will be generated automatically.
590
+ * @param callback - If provided, account is created unverified and callback receives confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler. If omitted, account is immediately verified.
591
+ * @returns The created account record
592
+ * @throws {EmailTakenError} Email is already registered
593
+ * @throws {InvalidPasswordError} Password doesn't meet length requirements
594
+ */
595
+ async createUser(credentials, userId, callback) {
596
+ validateEmail(credentials.email);
597
+ this.validatePassword(credentials.password);
598
+ const existing = await this.queries.findAccountByEmail(credentials.email);
599
+ if (existing) {
600
+ throw new EmailTakenError();
601
+ }
602
+ const finalUserId = userId || this.generateAutoUserId();
603
+ const hashedPassword = import_hash.hash.encode(credentials.password);
604
+ const verified = typeof callback !== "function";
605
+ const account = await this.queries.createAccount({
606
+ userId: finalUserId,
607
+ email: credentials.email,
608
+ password: hashedPassword,
609
+ verified,
610
+ status: AuthStatus.Normal,
611
+ rolemask: 0
612
+ });
613
+ if (!verified && callback) {
614
+ await this.createConfirmationToken(account, credentials.email, callback);
615
+ }
616
+ return account;
617
+ }
618
+ /**
619
+ * Log in as another user (admin function).
620
+ * Creates a new session as the target user without requiring their password.
621
+ *
622
+ * @param identifier - Find user by accountId, email, or userId
623
+ * @throws {UserNotFoundError} No account matches the identifier
624
+ */
625
+ async loginAsUserBy(identifier) {
626
+ const account = await this.findAccountByIdentifier(identifier);
627
+ if (!account) {
628
+ throw new UserNotFoundError();
629
+ }
630
+ await this.auth.onLoginSuccessful(account, false);
631
+ }
632
+ /**
633
+ * Delete a user account and all associated data (admin function).
634
+ * Removes account, confirmations, remember tokens, and reset tokens.
635
+ *
636
+ * @param identifier - Find user by accountId, email, or userId
637
+ * @throws {UserNotFoundError} No account matches the identifier
638
+ */
639
+ async deleteUserBy(identifier) {
640
+ const account = await this.findAccountByIdentifier(identifier);
641
+ if (!account) {
642
+ throw new UserNotFoundError();
643
+ }
644
+ await this.queries.deleteAccount(account.id);
645
+ }
646
+ /**
647
+ * Add a role to a user's account (admin function).
648
+ * Uses bitwise OR to add role to existing rolemask.
649
+ *
650
+ * @param identifier - Find user by accountId, email, or userId
651
+ * @param role - Role bitmask to add (e.g., AuthRole.Admin)
652
+ * @throws {UserNotFoundError} No account matches the identifier
653
+ */
654
+ async addRoleForUserBy(identifier, role) {
655
+ const account = await this.findAccountByIdentifier(identifier);
656
+ if (!account) {
657
+ throw new UserNotFoundError();
658
+ }
659
+ const rolemask = account.rolemask | role;
660
+ await this.queries.updateAccount(account.id, { rolemask });
661
+ }
662
+ /**
663
+ * Remove a role from a user's account (admin function).
664
+ * Uses bitwise operations to remove role from rolemask.
665
+ *
666
+ * @param identifier - Find user by accountId, email, or userId
667
+ * @param role - Role bitmask to remove (e.g., AuthRole.Admin)
668
+ * @throws {UserNotFoundError} No account matches the identifier
669
+ */
670
+ async removeRoleForUserBy(identifier, role) {
671
+ const account = await this.findAccountByIdentifier(identifier);
672
+ if (!account) {
673
+ throw new UserNotFoundError();
674
+ }
675
+ const rolemask = account.rolemask & ~role;
676
+ await this.queries.updateAccount(account.id, { rolemask });
677
+ }
678
+ /**
679
+ * Check if a user has a specific role (admin function).
680
+ *
681
+ * @param identifier - Find user by accountId, email, or userId
682
+ * @param role - Role bitmask to check (e.g., AuthRole.Admin)
683
+ * @returns true if user has the role, false otherwise
684
+ * @throws {UserNotFoundError} No account matches the identifier
685
+ */
686
+ async hasRoleForUserBy(identifier, role) {
687
+ const account = await this.findAccountByIdentifier(identifier);
688
+ if (!account) {
689
+ throw new UserNotFoundError();
690
+ }
691
+ return (account.rolemask & role) === role;
692
+ }
693
+ /**
694
+ * Change a user's password (admin function).
695
+ * Does not require knowing the current password.
696
+ *
697
+ * @param identifier - Find user by accountId, email, or userId
698
+ * @param password - New password (will be hashed)
699
+ * @throws {UserNotFoundError} No account matches the identifier
700
+ * @throws {InvalidPasswordError} New password doesn't meet requirements
701
+ */
702
+ async changePasswordForUserBy(identifier, password) {
703
+ this.validatePassword(password);
704
+ const account = await this.findAccountByIdentifier(identifier);
705
+ if (!account) {
706
+ throw new UserNotFoundError();
707
+ }
708
+ await this.queries.updateAccount(account.id, {
709
+ password: import_hash.hash.encode(password)
710
+ });
711
+ }
712
+ /**
713
+ * Change a user's account status (admin function).
714
+ *
715
+ * @param identifier - Find user by accountId, email, or userId
716
+ * @param status - New status (0=Normal, 1=Archived, 2=Banned, 3=Locked, etc.)
717
+ * @throws {UserNotFoundError} No account matches the identifier
718
+ */
719
+ async setStatusForUserBy(identifier, status) {
720
+ const account = await this.findAccountByIdentifier(identifier);
721
+ if (!account) {
722
+ throw new UserNotFoundError();
723
+ }
724
+ await this.queries.updateAccount(account.id, { status });
725
+ }
726
+ /**
727
+ * Initiate password reset for a user (admin function).
728
+ * Creates a reset token without rate limiting.
729
+ *
730
+ * @param identifier - Find user by accountId, email, or userId
731
+ * @param expiresAfter - Token expiration (default: 6h). Accepts ms format like '1h', '30m'
732
+ * @param callback - Called with reset token. Create a URL like /reset/{token} and call confirmResetPassword() in that handler
733
+ * @throws {UserNotFoundError} No account matches the identifier
734
+ * @throws {EmailNotVerifiedError} Account exists but email is not verified
735
+ */
736
+ async initiatePasswordResetForUserBy(identifier, expiresAfter = null, callback) {
737
+ const account = await this.findAccountByIdentifier(identifier);
738
+ if (!account) {
739
+ throw new UserNotFoundError();
740
+ }
741
+ if (!account.verified) {
742
+ throw new EmailNotVerifiedError();
743
+ }
744
+ const expiry = !expiresAfter ? (0, import_ms.default)("6h") : (0, import_ms.default)(expiresAfter);
745
+ const token = import_hash.hash.encode(account.email);
746
+ const expires = new Date(Date.now() + expiry);
747
+ await this.queries.createResetToken({
748
+ accountId: account.id,
749
+ token,
750
+ expires
751
+ });
752
+ if (callback) {
753
+ callback(token);
754
+ }
755
+ }
756
+ /**
757
+ * Force logout all sessions for a specific user (admin function).
758
+ * Increments force_logout counter and deletes all remember tokens.
759
+ * If target user is currently logged in, marks their session for logout.
760
+ *
761
+ * @param identifier - Find user by accountId, email, or userId
762
+ * @throws {UserNotFoundError} No account matches the identifier
763
+ */
764
+ async forceLogoutForUserBy(identifier) {
765
+ const account = await this.findAccountByIdentifier(identifier);
766
+ if (!account) {
767
+ throw new UserNotFoundError();
768
+ }
769
+ await this.queries.incrementForceLogout(account.id);
770
+ if (this.auth.getId() === account.id) {
771
+ this.req.session.auth.shouldForceLogout = true;
772
+ }
773
+ }
774
+ };
775
+
776
+ // src/auth-manager.ts
777
+ var import_hash4 = require("@prsm/hash");
778
+ var import_ms3 = __toESM(require("@prsm/ms"), 1);
779
+
780
+ // src/activity-logger.ts
781
+ var import_bowser = __toESM(require("bowser"), 1);
782
+ var ActivityLogger = class {
783
+ constructor(config) {
784
+ this.config = config;
785
+ this.enabled = config.activityLog?.enabled !== false;
786
+ this.maxEntries = config.activityLog?.maxEntries || 1e4;
787
+ this.allowedActions = config.activityLog?.actions || null;
788
+ this.tablePrefix = config.tablePrefix || "user_";
789
+ }
790
+ get activityTable() {
791
+ return `${this.tablePrefix}activity_log`;
792
+ }
793
+ parseUserAgent(userAgent) {
794
+ if (!userAgent) {
795
+ return { browser: null, os: null, device: null };
796
+ }
797
+ try {
798
+ const browser = import_bowser.default.getParser(userAgent);
799
+ const result = browser.getResult();
800
+ return {
801
+ browser: result.browser.name || null,
802
+ os: result.os.name || null,
803
+ device: result.platform.type || "desktop"
804
+ };
805
+ } catch (error) {
806
+ return this.parseUserAgentSimple(userAgent);
807
+ }
808
+ }
809
+ parseUserAgentSimple(userAgent) {
810
+ let browser = null;
811
+ if (userAgent.includes("Chrome")) browser = "Chrome";
812
+ else if (userAgent.includes("Firefox")) browser = "Firefox";
813
+ else if (userAgent.includes("Safari")) browser = "Safari";
814
+ else if (userAgent.includes("Edge")) browser = "Edge";
815
+ let os = null;
816
+ if (userAgent.includes("Windows")) os = "Windows";
817
+ else if (userAgent.includes("Mac OS")) os = "macOS";
818
+ else if (userAgent.includes("Linux")) os = "Linux";
819
+ else if (userAgent.includes("Android")) os = "Android";
820
+ else if (userAgent.includes("iOS")) os = "iOS";
821
+ let device = "desktop";
822
+ if (userAgent.includes("Mobile")) device = "mobile";
823
+ else if (userAgent.includes("Tablet")) device = "tablet";
824
+ return { browser, os, device };
825
+ }
826
+ getIpAddress(req) {
827
+ return req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || req.connection?.socket?.remoteAddress || null;
828
+ }
829
+ async logActivity(accountId, action, req, success = true, metadata = {}) {
830
+ if (!this.enabled) return;
831
+ if (this.allowedActions && !this.allowedActions.includes(action)) {
832
+ return;
833
+ }
834
+ const userAgent = (typeof req.get === "function" ? req.get("User-Agent") : req.headers?.["user-agent"]) || null;
835
+ const ip = this.getIpAddress(req);
836
+ const parsed = this.parseUserAgent(userAgent);
837
+ try {
838
+ await this.config.db.query(
839
+ `
840
+ INSERT INTO ${this.activityTable}
841
+ (account_id, action, ip_address, user_agent, browser, os, device, success, metadata)
842
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
843
+ `,
844
+ [accountId, action, ip, userAgent, parsed.browser, parsed.os, parsed.device, success, Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null]
845
+ );
846
+ if (Math.random() < 0.01) {
847
+ await this.cleanup();
848
+ }
849
+ } catch (error) {
850
+ console.error("ActivityLogger: Failed to log activity:", error);
851
+ }
852
+ }
853
+ async cleanup() {
854
+ if (!this.enabled) return;
855
+ try {
856
+ await this.config.db.query(
857
+ `
858
+ DELETE FROM ${this.activityTable}
859
+ WHERE id NOT IN (
860
+ SELECT id FROM ${this.activityTable}
861
+ ORDER BY created_at DESC
862
+ LIMIT $1
863
+ )
864
+ `,
865
+ [this.maxEntries]
866
+ );
867
+ } catch (error) {
868
+ console.error("ActivityLogger: Failed to cleanup old entries:", error);
869
+ }
870
+ }
871
+ async getRecentActivity(limit = 100, accountId) {
872
+ if (!this.enabled) return [];
873
+ try {
874
+ let sql = `
875
+ SELECT
876
+ al.*,
877
+ a.email
878
+ FROM ${this.activityTable} al
879
+ LEFT JOIN ${this.tablePrefix}accounts a ON al.account_id = a.id
880
+ `;
881
+ const params = [];
882
+ if (accountId !== void 0) {
883
+ sql += " WHERE al.account_id = $1";
884
+ params.push(accountId);
885
+ }
886
+ sql += ` ORDER BY al.created_at DESC LIMIT $${params.length + 1}`;
887
+ params.push(Math.min(limit, 1e3));
888
+ const result = await this.config.db.query(sql, params);
889
+ return result.rows.map((row) => ({
890
+ ...row,
891
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
892
+ }));
893
+ } catch (error) {
894
+ console.error("ActivityLogger: Failed to get recent activity:", error);
895
+ return [];
896
+ }
897
+ }
898
+ async getActivityStats() {
899
+ if (!this.enabled) {
900
+ return {
901
+ totalEntries: 0,
902
+ uniqueUsers: 0,
903
+ recentLogins: 0,
904
+ failedAttempts: 0
905
+ };
906
+ }
907
+ try {
908
+ const [total, unique, recent, failed] = await Promise.all([
909
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable}`),
910
+ this.config.db.query(`SELECT COUNT(DISTINCT account_id) as count FROM ${this.activityTable} WHERE account_id IS NOT NULL`),
911
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable} WHERE action = 'login' AND created_at > NOW() - INTERVAL '24 hours'`),
912
+ this.config.db.query(`SELECT COUNT(*) as count FROM ${this.activityTable} WHERE success = false AND created_at > NOW() - INTERVAL '24 hours'`)
913
+ ]);
914
+ return {
915
+ totalEntries: parseInt(total.rows[0]?.count || "0"),
916
+ uniqueUsers: parseInt(unique.rows[0]?.count || "0"),
917
+ recentLogins: parseInt(recent.rows[0]?.count || "0"),
918
+ failedAttempts: parseInt(failed.rows[0]?.count || "0")
919
+ };
920
+ } catch (error) {
921
+ console.error("ActivityLogger: Failed to get activity stats:", error);
922
+ return {
923
+ totalEntries: 0,
924
+ uniqueUsers: 0,
925
+ recentLogins: 0,
926
+ failedAttempts: 0
927
+ };
928
+ }
929
+ }
930
+ };
931
+
932
+ // src/providers/base-provider.ts
933
+ var BaseOAuthProvider = class {
934
+ constructor(config, authConfig, authManager) {
935
+ this.config = config;
936
+ this.authConfig = authConfig;
937
+ this.authManager = authManager;
938
+ }
939
+ async handleCallback(req) {
940
+ const userData = await this.getUserData(req);
941
+ await this.processOAuthLogin(userData, req);
942
+ }
943
+ async processOAuthLogin(userData, req) {
944
+ const { queries } = this.authManager;
945
+ const providerName = this.getProviderName();
946
+ const existingProvider = await queries.findProviderByProviderIdAndType(userData.id, providerName);
947
+ if (existingProvider) {
948
+ const account2 = await queries.findAccountById(existingProvider.account_id);
949
+ if (account2) {
950
+ await this.authManager.onLoginSuccessful(account2, false);
951
+ return;
952
+ }
953
+ }
954
+ if (userData.email) {
955
+ const existingAccount = await queries.findAccountByEmail(userData.email);
956
+ if (existingAccount) {
957
+ throw new Error("You already have an account associated with this email address.");
958
+ }
959
+ }
960
+ let userId;
961
+ if (this.authConfig.createUser) {
962
+ userId = await this.authConfig.createUser(userData);
963
+ } else {
964
+ userId = crypto.randomUUID();
965
+ }
966
+ const account = await queries.createAccount({
967
+ userId,
968
+ email: userData.email,
969
+ password: null,
970
+ verified: true,
971
+ // OAuth providers are pre-verified
972
+ status: 0,
973
+ // AuthStatus.Normal
974
+ rolemask: 0
975
+ });
976
+ await queries.createProvider({
977
+ accountId: account.id,
978
+ provider: providerName,
979
+ providerId: userData.id,
980
+ providerEmail: userData.email,
981
+ providerUsername: userData.username || null,
982
+ providerName: userData.name || null,
983
+ providerAvatar: userData.avatar || null
984
+ });
985
+ await this.authManager.onLoginSuccessful(account, false);
986
+ }
987
+ async exchangeCodeForToken(code, tokenUrl) {
988
+ const response = await fetch(tokenUrl, {
989
+ method: "POST",
990
+ headers: {
991
+ Accept: "application/json",
992
+ "Content-Type": "application/x-www-form-urlencoded"
993
+ },
994
+ body: new URLSearchParams({
995
+ client_id: this.config.clientId,
996
+ client_secret: this.config.clientSecret,
997
+ code,
998
+ redirect_uri: this.config.redirectUri,
999
+ grant_type: "authorization_code"
1000
+ })
1001
+ });
1002
+ if (!response.ok) {
1003
+ throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
1004
+ }
1005
+ const data = await response.json();
1006
+ if (!data.access_token) {
1007
+ throw new Error("No access token received from OAuth provider");
1008
+ }
1009
+ return data.access_token;
1010
+ }
1011
+ async fetchUserFromAPI(accessToken, apiUrl) {
1012
+ const response = await fetch(apiUrl, {
1013
+ headers: {
1014
+ Authorization: `Bearer ${accessToken}`,
1015
+ Accept: "application/json"
1016
+ }
1017
+ });
1018
+ if (!response.ok) {
1019
+ throw new Error(`Failed to fetch user data: ${response.status} ${response.statusText}`);
1020
+ }
1021
+ return response.json();
1022
+ }
1023
+ };
1024
+
1025
+ // src/providers/github-provider.ts
1026
+ var GitHubProvider = class extends BaseOAuthProvider {
1027
+ constructor(config, authConfig, authManager) {
1028
+ super(config, authConfig, authManager);
1029
+ }
1030
+ getAuthUrl(state, scopes) {
1031
+ const params = new URLSearchParams({
1032
+ client_id: this.config.clientId,
1033
+ redirect_uri: this.config.redirectUri,
1034
+ scope: scopes?.join(" ") || "user:email",
1035
+ state: state || Math.random().toString(36).substring(2),
1036
+ response_type: "code"
1037
+ });
1038
+ return `https://github.com/login/oauth/authorize?${params}`;
1039
+ }
1040
+ async getUserData(req) {
1041
+ const code = req.query.code;
1042
+ if (!code) {
1043
+ throw new Error("No authorization code provided");
1044
+ }
1045
+ const accessToken = await this.exchangeCodeForToken(code, "https://github.com/login/oauth/access_token");
1046
+ const [user, emails] = await Promise.all([this.fetchUserFromAPI(accessToken, "https://api.github.com/user"), this.fetchUserFromAPI(accessToken, "https://api.github.com/user/emails")]);
1047
+ const primaryEmail = Array.isArray(emails) ? emails.find((email) => email.primary)?.email : null;
1048
+ if (!primaryEmail) {
1049
+ throw new Error("No primary email found in GitHub account");
1050
+ }
1051
+ return {
1052
+ id: user.id.toString(),
1053
+ email: primaryEmail,
1054
+ username: user.login,
1055
+ name: user.name || user.login,
1056
+ avatar: user.avatar_url
1057
+ };
1058
+ }
1059
+ getProviderName() {
1060
+ return "github";
1061
+ }
1062
+ };
1063
+
1064
+ // src/providers/google-provider.ts
1065
+ var GoogleProvider = class extends BaseOAuthProvider {
1066
+ constructor(config, authConfig, authManager) {
1067
+ super(config, authConfig, authManager);
1068
+ }
1069
+ getAuthUrl(state, scopes) {
1070
+ const params = new URLSearchParams({
1071
+ client_id: this.config.clientId,
1072
+ redirect_uri: this.config.redirectUri,
1073
+ scope: scopes?.join(" ") || "openid profile email",
1074
+ state: state || Math.random().toString(36).substring(2),
1075
+ response_type: "code",
1076
+ access_type: "offline",
1077
+ prompt: "consent"
1078
+ });
1079
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
1080
+ }
1081
+ async getUserData(req) {
1082
+ const code = req.query.code;
1083
+ if (!code) {
1084
+ throw new Error("No authorization code provided");
1085
+ }
1086
+ const accessToken = await this.exchangeCodeForToken(code, "https://oauth2.googleapis.com/token");
1087
+ const user = await this.fetchUserFromAPI(accessToken, "https://www.googleapis.com/oauth2/v2/userinfo");
1088
+ if (!user.email) {
1089
+ throw new Error("No email found in Google account");
1090
+ }
1091
+ return {
1092
+ id: user.id,
1093
+ email: user.email,
1094
+ username: user.email.split("@")[0],
1095
+ // use email prefix as username
1096
+ name: user.name,
1097
+ avatar: user.picture
1098
+ };
1099
+ }
1100
+ getProviderName() {
1101
+ return "google";
1102
+ }
1103
+ };
1104
+
1105
+ // src/providers/azure-provider.ts
1106
+ var AzureProvider = class extends BaseOAuthProvider {
1107
+ constructor(config, authConfig, authManager) {
1108
+ super(config, authConfig, authManager);
1109
+ }
1110
+ getAuthUrl(state, scopes) {
1111
+ const azureConfig = this.config;
1112
+ const params = new URLSearchParams({
1113
+ client_id: azureConfig.clientId,
1114
+ redirect_uri: azureConfig.redirectUri,
1115
+ scope: scopes?.join(" ") || "openid profile email User.Read",
1116
+ state: state || Math.random().toString(36).substring(2),
1117
+ response_type: "code",
1118
+ response_mode: "query"
1119
+ });
1120
+ return `https://login.microsoftonline.com/${azureConfig.tenantId}/oauth2/v2.0/authorize?${params}`;
1121
+ }
1122
+ async getUserData(req) {
1123
+ const code = req.query.code;
1124
+ if (!code) {
1125
+ throw new Error("No authorization code provided");
1126
+ }
1127
+ const azureConfig = this.config;
1128
+ const accessToken = await this.exchangeCodeForToken(code, `https://login.microsoftonline.com/${azureConfig.tenantId}/oauth2/v2.0/token`);
1129
+ const user = await this.fetchUserFromAPI(accessToken, "https://graph.microsoft.com/v1.0/me");
1130
+ if (!user.mail && !user.userPrincipalName) {
1131
+ throw new Error("No email found in Azure account");
1132
+ }
1133
+ return {
1134
+ id: user.id,
1135
+ email: user.mail || user.userPrincipalName,
1136
+ username: user.mailNickname || user.userPrincipalName?.split("@")[0],
1137
+ name: user.displayName,
1138
+ avatar: void 0
1139
+ // Azure doesn't provide avatar in basic profile
1140
+ };
1141
+ }
1142
+ getProviderName() {
1143
+ return "azure";
1144
+ }
1145
+ async exchangeCodeForToken(code, tokenUrl) {
1146
+ const azureConfig = this.config;
1147
+ const response = await fetch(tokenUrl, {
1148
+ method: "POST",
1149
+ headers: {
1150
+ Accept: "application/json",
1151
+ "Content-Type": "application/x-www-form-urlencoded"
1152
+ },
1153
+ body: new URLSearchParams({
1154
+ client_id: azureConfig.clientId,
1155
+ client_secret: azureConfig.clientSecret,
1156
+ code,
1157
+ redirect_uri: azureConfig.redirectUri,
1158
+ grant_type: "authorization_code",
1159
+ scope: "openid profile email User.Read"
1160
+ })
1161
+ });
1162
+ if (!response.ok) {
1163
+ throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
1164
+ }
1165
+ const data = await response.json();
1166
+ if (!data.access_token) {
1167
+ throw new Error("No access token received from Azure");
1168
+ }
1169
+ return data.access_token;
1170
+ }
1171
+ };
1172
+
1173
+ // src/two-factor/totp-provider.ts
1174
+ var import_totp = __toESM(require("@eaccess/totp"), 1);
1175
+ var import_hash2 = require("@prsm/hash");
1176
+ var TotpProvider = class {
1177
+ constructor(config) {
1178
+ this.config = config;
1179
+ }
1180
+ generateSecret() {
1181
+ return import_totp.default.createSecret();
1182
+ }
1183
+ generateQRCode(email, secret) {
1184
+ const issuer = this.config.twoFactor?.issuer || "EasyAccess";
1185
+ return import_totp.default.createTotpKeyUriForQrCode(issuer, email, secret);
1186
+ }
1187
+ verify(secret, code) {
1188
+ const window = this.config.twoFactor?.totpWindow || 1;
1189
+ return import_totp.default.verifyTotp(secret, code, window);
1190
+ }
1191
+ generateBackupCodes(count = 10) {
1192
+ const codes = [];
1193
+ for (let i = 0; i < count; i++) {
1194
+ const chars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
1195
+ let code = "";
1196
+ for (let j = 0; j < 8; j++) {
1197
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
1198
+ }
1199
+ codes.push(code);
1200
+ }
1201
+ return codes;
1202
+ }
1203
+ hashBackupCodes(codes) {
1204
+ return codes.map((code) => import_hash2.hash.encode(code));
1205
+ }
1206
+ verifyBackupCode(hashedCodes, inputCode) {
1207
+ for (let i = 0; i < hashedCodes.length; i++) {
1208
+ if (import_hash2.hash.verify(hashedCodes[i], inputCode.toUpperCase())) {
1209
+ return { isValid: true, index: i };
1210
+ }
1211
+ }
1212
+ return { isValid: false, index: -1 };
1213
+ }
1214
+ maskEmail(email) {
1215
+ const [username, domain] = email.split("@");
1216
+ if (username.length <= 2) {
1217
+ return `${username[0]}***@${domain}`;
1218
+ }
1219
+ return `${username[0]}${"*".repeat(username.length - 2)}${username[username.length - 1]}@${domain}`;
1220
+ }
1221
+ };
1222
+
1223
+ // src/two-factor/otp-provider.ts
1224
+ var import_ms2 = __toESM(require("@prsm/ms"), 1);
1225
+ var import_hash3 = require("@prsm/hash");
1226
+ var OtpProvider = class {
1227
+ constructor(config) {
1228
+ this.config = config;
1229
+ this.queries = new AuthQueries(config);
1230
+ }
1231
+ generateOTP() {
1232
+ const length = this.config.twoFactor?.codeLength || 6;
1233
+ let otp = "";
1234
+ for (let i = 0; i < length; i++) {
1235
+ otp += Math.floor(Math.random() * 10).toString();
1236
+ }
1237
+ return otp;
1238
+ }
1239
+ generateSelector() {
1240
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1241
+ let selector = "";
1242
+ for (let i = 0; i < 32; i++) {
1243
+ selector += chars.charAt(Math.floor(Math.random() * chars.length));
1244
+ }
1245
+ return selector;
1246
+ }
1247
+ async createAndStoreOTP(accountId, mechanism) {
1248
+ const otp = this.generateOTP();
1249
+ const selector = this.generateSelector();
1250
+ const tokenHash = import_hash3.hash.encode(otp);
1251
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
1252
+ const expiresAt = new Date(Date.now() + (0, import_ms2.default)(expiryDuration));
1253
+ await this.queries.deleteTwoFactorTokensByAccountAndMechanism(accountId, mechanism);
1254
+ await this.queries.createTwoFactorToken({
1255
+ accountId,
1256
+ mechanism,
1257
+ selector,
1258
+ tokenHash,
1259
+ expiresAt
1260
+ });
1261
+ return { otp, selector };
1262
+ }
1263
+ async verifyOTP(selector, inputCode) {
1264
+ const token = await this.queries.findTwoFactorTokenBySelector(selector);
1265
+ if (!token) {
1266
+ return { isValid: false };
1267
+ }
1268
+ if (token.expires_at <= /* @__PURE__ */ new Date()) {
1269
+ await this.queries.deleteTwoFactorToken(token.id);
1270
+ return { isValid: false };
1271
+ }
1272
+ const isValid = import_hash3.hash.verify(token.token_hash, inputCode);
1273
+ if (isValid) {
1274
+ await this.queries.deleteTwoFactorToken(token.id);
1275
+ return { isValid: true, token };
1276
+ }
1277
+ return { isValid: false };
1278
+ }
1279
+ maskPhone(phone) {
1280
+ if (phone.length < 4) {
1281
+ return phone.replace(/./g, "*");
1282
+ }
1283
+ if (phone.startsWith("+")) {
1284
+ return phone[0] + phone[1] + "*".repeat(phone.length - 3) + phone.slice(-2);
1285
+ }
1286
+ return phone[0] + "*".repeat(phone.length - 3) + phone.slice(-2);
1287
+ }
1288
+ maskEmail(email) {
1289
+ const [username, domain] = email.split("@");
1290
+ if (username.length <= 2) {
1291
+ return `${username[0]}***@${domain}`;
1292
+ }
1293
+ return `${username[0]}${"*".repeat(username.length - 2)}${username[username.length - 1]}@${domain}`;
1294
+ }
1295
+ };
1296
+
1297
+ // src/two-factor/two-factor-manager.ts
1298
+ var TwoFactorManager = class {
1299
+ constructor(req, res, config) {
1300
+ // setup & management
1301
+ this.setup = {
1302
+ totp: async (requireVerification = false) => {
1303
+ const accountId = this.getAccountId();
1304
+ const email = this.getEmail();
1305
+ if (!accountId || !email) {
1306
+ throw new UserNotLoggedInError();
1307
+ }
1308
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1309
+ if (existingMethod?.verified) {
1310
+ throw new TwoFactorAlreadyEnabledError();
1311
+ }
1312
+ const secret = this.totpProvider.generateSecret();
1313
+ const qrCode = this.totpProvider.generateQRCode(email, secret);
1314
+ let backupCodes;
1315
+ if (!requireVerification) {
1316
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1317
+ backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1318
+ }
1319
+ const hashedBackupCodes = backupCodes ? this.totpProvider.hashBackupCodes(backupCodes) : void 0;
1320
+ const verified = !requireVerification;
1321
+ if (existingMethod) {
1322
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1323
+ secret,
1324
+ backup_codes: hashedBackupCodes || null,
1325
+ verified
1326
+ });
1327
+ } else {
1328
+ await this.queries.createTwoFactorMethod({
1329
+ accountId,
1330
+ mechanism: 1 /* TOTP */,
1331
+ secret,
1332
+ backupCodes: hashedBackupCodes,
1333
+ verified
1334
+ });
1335
+ }
1336
+ if (verified) {
1337
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "totp" });
1338
+ }
1339
+ return { secret, qrCode, backupCodes };
1340
+ },
1341
+ email: async (email, requireVerification = false) => {
1342
+ const accountId = this.getAccountId();
1343
+ const userEmail = email || this.getEmail();
1344
+ if (!accountId || !userEmail) {
1345
+ throw new UserNotLoggedInError();
1346
+ }
1347
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 2 /* EMAIL */);
1348
+ if (existingMethod?.verified) {
1349
+ throw new TwoFactorAlreadyEnabledError();
1350
+ }
1351
+ const verified = !requireVerification;
1352
+ if (existingMethod) {
1353
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1354
+ secret: userEmail,
1355
+ verified
1356
+ });
1357
+ } else {
1358
+ await this.queries.createTwoFactorMethod({
1359
+ accountId,
1360
+ mechanism: 2 /* EMAIL */,
1361
+ secret: userEmail,
1362
+ verified
1363
+ });
1364
+ }
1365
+ if (verified) {
1366
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "email" });
1367
+ }
1368
+ },
1369
+ sms: async (phone, requireVerification = true) => {
1370
+ const accountId = this.getAccountId();
1371
+ if (!accountId) {
1372
+ throw new UserNotLoggedInError();
1373
+ }
1374
+ const existingMethod = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 3 /* SMS */);
1375
+ if (existingMethod?.verified) {
1376
+ throw new TwoFactorAlreadyEnabledError();
1377
+ }
1378
+ const verified = !requireVerification;
1379
+ if (existingMethod) {
1380
+ await this.queries.updateTwoFactorMethod(existingMethod.id, {
1381
+ secret: phone,
1382
+ verified
1383
+ });
1384
+ } else {
1385
+ await this.queries.createTwoFactorMethod({
1386
+ accountId,
1387
+ mechanism: 3 /* SMS */,
1388
+ secret: phone,
1389
+ verified
1390
+ });
1391
+ }
1392
+ if (verified) {
1393
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "sms" });
1394
+ }
1395
+ }
1396
+ };
1397
+ this.complete = {
1398
+ totp: async (code) => {
1399
+ const accountId = this.getAccountId();
1400
+ if (!accountId) {
1401
+ throw new UserNotLoggedInError();
1402
+ }
1403
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1404
+ if (!method || !method.secret) {
1405
+ throw new TwoFactorNotSetupError();
1406
+ }
1407
+ if (method.verified) {
1408
+ throw new TwoFactorAlreadyEnabledError();
1409
+ }
1410
+ const isValid = this.totpProvider.verify(method.secret, code);
1411
+ if (!isValid) {
1412
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "totp", reason: "invalid_code" });
1413
+ throw new InvalidTwoFactorCodeError();
1414
+ }
1415
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1416
+ const backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1417
+ const hashedBackupCodes = this.totpProvider.hashBackupCodes(backupCodes);
1418
+ await this.queries.updateTwoFactorMethod(method.id, {
1419
+ verified: true,
1420
+ backup_codes: hashedBackupCodes,
1421
+ last_used_at: /* @__PURE__ */ new Date()
1422
+ });
1423
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: "totp" });
1424
+ return backupCodes;
1425
+ },
1426
+ email: async (code) => {
1427
+ await this.completeOtpSetup(2 /* EMAIL */, code);
1428
+ },
1429
+ sms: async (code) => {
1430
+ await this.completeOtpSetup(3 /* SMS */, code);
1431
+ }
1432
+ };
1433
+ // verification during login flow
1434
+ this.verify = {
1435
+ totp: async (code) => {
1436
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1437
+ if (!twoFactorState) {
1438
+ throw new UserNotLoggedInError();
1439
+ }
1440
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, 1 /* TOTP */);
1441
+ if (!method || !method.verified || !method.secret) {
1442
+ throw new TwoFactorNotSetupError();
1443
+ }
1444
+ const isValid = this.totpProvider.verify(method.secret, code);
1445
+ if (!isValid) {
1446
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "totp", reason: "invalid_code" });
1447
+ throw new InvalidTwoFactorCodeError();
1448
+ }
1449
+ await this.queries.updateTwoFactorMethod(method.id, {
1450
+ last_used_at: /* @__PURE__ */ new Date()
1451
+ });
1452
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorVerified, this.req, true, { mechanism: "totp" });
1453
+ },
1454
+ email: async (code) => {
1455
+ await this.verifyOtp(2 /* EMAIL */, code);
1456
+ },
1457
+ sms: async (code) => {
1458
+ await this.verifyOtp(3 /* SMS */, code);
1459
+ },
1460
+ backupCode: async (code) => {
1461
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1462
+ if (!twoFactorState) {
1463
+ throw new UserNotLoggedInError();
1464
+ }
1465
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, 1 /* TOTP */);
1466
+ if (!method || !method.verified || !method.backup_codes) {
1467
+ throw new TwoFactorNotSetupError();
1468
+ }
1469
+ const { isValid, index } = this.totpProvider.verifyBackupCode(method.backup_codes, code);
1470
+ if (!isValid) {
1471
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "backup_code", reason: "invalid_code" });
1472
+ throw new InvalidBackupCodeError();
1473
+ }
1474
+ const updatedBackupCodes = [...method.backup_codes];
1475
+ updatedBackupCodes.splice(index, 1);
1476
+ await this.queries.updateTwoFactorMethod(method.id, {
1477
+ backup_codes: updatedBackupCodes,
1478
+ last_used_at: /* @__PURE__ */ new Date()
1479
+ });
1480
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.BackupCodeUsed, this.req, true, { remaining_codes: updatedBackupCodes.length });
1481
+ },
1482
+ otp: async (code) => {
1483
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1484
+ if (!twoFactorState) {
1485
+ throw new UserNotLoggedInError();
1486
+ }
1487
+ const availableMechanisms = twoFactorState.availableMechanisms.filter((m) => m === 2 /* EMAIL */ || m === 3 /* SMS */);
1488
+ if (availableMechanisms.length === 0) {
1489
+ throw new TwoFactorNotSetupError();
1490
+ }
1491
+ for (const mechanism of availableMechanisms) {
1492
+ try {
1493
+ await this.verifyOtp(mechanism, code);
1494
+ return;
1495
+ } catch (error) {
1496
+ continue;
1497
+ }
1498
+ }
1499
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, { mechanism: "otp", reason: "invalid_code" });
1500
+ throw new InvalidTwoFactorCodeError();
1501
+ }
1502
+ };
1503
+ this.req = req;
1504
+ this.res = res;
1505
+ this.config = config;
1506
+ this.queries = new AuthQueries(config);
1507
+ this.activityLogger = new ActivityLogger(config);
1508
+ this.totpProvider = new TotpProvider(config);
1509
+ this.otpProvider = new OtpProvider(config);
1510
+ }
1511
+ getAccountId() {
1512
+ return this.req.session?.auth?.accountId || null;
1513
+ }
1514
+ getEmail() {
1515
+ return this.req.session?.auth?.email || null;
1516
+ }
1517
+ // status queries
1518
+ async isEnabled() {
1519
+ const accountId = this.getAccountId();
1520
+ if (!accountId) return false;
1521
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1522
+ return methods.some((method) => method.verified);
1523
+ }
1524
+ async totpEnabled() {
1525
+ const accountId = this.getAccountId();
1526
+ if (!accountId) return false;
1527
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1528
+ return method?.verified || false;
1529
+ }
1530
+ async emailEnabled() {
1531
+ const accountId = this.getAccountId();
1532
+ if (!accountId) return false;
1533
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 2 /* EMAIL */);
1534
+ return method?.verified || false;
1535
+ }
1536
+ async smsEnabled() {
1537
+ const accountId = this.getAccountId();
1538
+ if (!accountId) return false;
1539
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 3 /* SMS */);
1540
+ return method?.verified || false;
1541
+ }
1542
+ async getEnabledMethods() {
1543
+ const accountId = this.getAccountId();
1544
+ if (!accountId) return [];
1545
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1546
+ return methods.filter((method) => method.verified).map((method) => method.mechanism);
1547
+ }
1548
+ async completeOtpSetup(mechanism, code) {
1549
+ const accountId = this.getAccountId();
1550
+ if (!accountId) {
1551
+ throw new UserNotLoggedInError();
1552
+ }
1553
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1554
+ if (!method) {
1555
+ throw new TwoFactorNotSetupError();
1556
+ }
1557
+ if (method.verified) {
1558
+ throw new TwoFactorAlreadyEnabledError();
1559
+ }
1560
+ await this.queries.updateTwoFactorMethod(method.id, {
1561
+ verified: true,
1562
+ last_used_at: /* @__PURE__ */ new Date()
1563
+ });
1564
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorSetup, this.req, true, { mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms" });
1565
+ }
1566
+ async verifyOtp(mechanism, code) {
1567
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1568
+ if (!twoFactorState) {
1569
+ throw new UserNotLoggedInError();
1570
+ }
1571
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(twoFactorState.accountId, mechanism);
1572
+ if (!method || !method.verified) {
1573
+ throw new TwoFactorNotSetupError();
1574
+ }
1575
+ const selector = mechanism === 2 /* EMAIL */ ? this.req.session?.auth?.awaitingTwoFactor?.selectors?.email : this.req.session?.auth?.awaitingTwoFactor?.selectors?.sms;
1576
+ if (!selector) {
1577
+ throw new InvalidTwoFactorCodeError();
1578
+ }
1579
+ const { isValid } = await this.otpProvider.verifyOTP(selector, code);
1580
+ if (!isValid) {
1581
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorFailed, this.req, false, {
1582
+ mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms",
1583
+ reason: "invalid_code"
1584
+ });
1585
+ throw new InvalidTwoFactorCodeError();
1586
+ }
1587
+ await this.queries.updateTwoFactorMethod(method.id, {
1588
+ last_used_at: /* @__PURE__ */ new Date()
1589
+ });
1590
+ await this.activityLogger.logActivity(twoFactorState.accountId, AuthActivityAction.TwoFactorVerified, this.req, true, { mechanism: mechanism === 2 /* EMAIL */ ? "email" : "sms" });
1591
+ }
1592
+ // management
1593
+ async disable(mechanism) {
1594
+ const accountId = this.getAccountId();
1595
+ if (!accountId) {
1596
+ throw new UserNotLoggedInError();
1597
+ }
1598
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1599
+ if (!method) {
1600
+ throw new TwoFactorNotSetupError();
1601
+ }
1602
+ await this.queries.deleteTwoFactorMethod(method.id);
1603
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.TwoFactorDisabled, this.req, true, {
1604
+ mechanism: mechanism === 1 /* TOTP */ ? "totp" : mechanism === 2 /* EMAIL */ ? "email" : "sms"
1605
+ });
1606
+ }
1607
+ async generateNewBackupCodes() {
1608
+ const accountId = this.getAccountId();
1609
+ if (!accountId) {
1610
+ throw new UserNotLoggedInError();
1611
+ }
1612
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, 1 /* TOTP */);
1613
+ if (!method || !method.verified) {
1614
+ throw new TwoFactorNotSetupError();
1615
+ }
1616
+ const backupCodesCount = this.config.twoFactor?.backupCodesCount || 10;
1617
+ const backupCodes = this.totpProvider.generateBackupCodes(backupCodesCount);
1618
+ const hashedBackupCodes = this.totpProvider.hashBackupCodes(backupCodes);
1619
+ await this.queries.updateTwoFactorMethod(method.id, {
1620
+ backup_codes: hashedBackupCodes
1621
+ });
1622
+ return backupCodes;
1623
+ }
1624
+ async getContact(mechanism) {
1625
+ const accountId = this.getAccountId();
1626
+ if (!accountId) {
1627
+ return null;
1628
+ }
1629
+ const method = await this.queries.findTwoFactorMethodByAccountAndMechanism(accountId, mechanism);
1630
+ return method?.secret || null;
1631
+ }
1632
+ // challenge creation (used during login)
1633
+ async createChallenge(accountId) {
1634
+ const methods = await this.queries.findTwoFactorMethodsByAccountId(accountId);
1635
+ const verifiedMethods = methods.filter((method) => method.verified);
1636
+ const challenge = {
1637
+ selectors: {}
1638
+ };
1639
+ for (const method of verifiedMethods) {
1640
+ switch (method.mechanism) {
1641
+ case 1 /* TOTP */:
1642
+ challenge.totp = true;
1643
+ break;
1644
+ case 2 /* EMAIL */:
1645
+ if (method.secret) {
1646
+ const { otp, selector } = await this.otpProvider.createAndStoreOTP(accountId, method.mechanism);
1647
+ challenge.email = {
1648
+ otpValue: otp,
1649
+ maskedContact: this.otpProvider.maskEmail(method.secret)
1650
+ };
1651
+ challenge.selectors.email = selector;
1652
+ }
1653
+ break;
1654
+ case 3 /* SMS */:
1655
+ if (method.secret) {
1656
+ const { otp, selector } = await this.otpProvider.createAndStoreOTP(accountId, method.mechanism);
1657
+ challenge.sms = {
1658
+ otpValue: otp,
1659
+ maskedContact: this.otpProvider.maskPhone(method.secret)
1660
+ };
1661
+ challenge.selectors.sms = selector;
1662
+ }
1663
+ break;
1664
+ }
1665
+ }
1666
+ return challenge;
1667
+ }
1668
+ };
1669
+
1670
+ // src/auth-manager.ts
1671
+ var AuthManager = class {
1672
+ constructor(req, res, config) {
1673
+ this.req = req;
1674
+ this.res = res;
1675
+ this.config = config;
1676
+ this.queries = new AuthQueries(config);
1677
+ this.activityLogger = new ActivityLogger(config);
1678
+ this.providers = this.initializeProviders();
1679
+ this.twoFactor = new TwoFactorManager(req, res, config);
1680
+ }
1681
+ initializeProviders() {
1682
+ const providers = {};
1683
+ if (this.config.providers?.github) {
1684
+ providers.github = new GitHubProvider(this.config.providers.github, this.config, this);
1685
+ }
1686
+ if (this.config.providers?.google) {
1687
+ providers.google = new GoogleProvider(this.config.providers.google, this.config, this);
1688
+ }
1689
+ if (this.config.providers?.azure) {
1690
+ providers.azure = new AzureProvider(this.config.providers.azure, this.config, this);
1691
+ }
1692
+ return providers;
1693
+ }
1694
+ generateAutoUserId() {
1695
+ return crypto.randomUUID();
1696
+ }
1697
+ async shouldRequire2FA(account) {
1698
+ const providers = await this.queries.findProvidersByAccountId(account.id);
1699
+ const hasOAuthProviders = providers.length > 0;
1700
+ if (hasOAuthProviders && !this.config.twoFactor?.requireForOAuth) {
1701
+ return false;
1702
+ }
1703
+ return true;
1704
+ }
1705
+ validatePassword(password) {
1706
+ const minLength = this.config.minPasswordLength || 8;
1707
+ const maxLength = this.config.maxPasswordLength || 64;
1708
+ if (typeof password !== "string") {
1709
+ throw new InvalidPasswordError();
1710
+ }
1711
+ if (password.length < minLength) {
1712
+ throw new InvalidPasswordError();
1713
+ }
1714
+ if (password.length > maxLength) {
1715
+ throw new InvalidPasswordError();
1716
+ }
1717
+ }
1718
+ getRoleMap() {
1719
+ return createMapFromEnum(AuthRole);
1720
+ }
1721
+ getStatusMap() {
1722
+ return createMapFromEnum(AuthStatus);
1723
+ }
1724
+ async getAuthAccount() {
1725
+ if (!this.req.session?.auth?.accountId) {
1726
+ return null;
1727
+ }
1728
+ return await this.queries.findAccountById(this.req.session.auth.accountId);
1729
+ }
1730
+ setRememberCookie(token, expires) {
1731
+ const cookieName = this.config.rememberCookieName || "remember_token";
1732
+ if (token === null) {
1733
+ this.res.clearCookie(cookieName);
1734
+ } else {
1735
+ this.res.cookie(cookieName, token, {
1736
+ expires,
1737
+ httpOnly: true,
1738
+ secure: this.req.secure
1739
+ });
1740
+ }
1741
+ }
1742
+ getRememberToken() {
1743
+ const { cookies } = this.req;
1744
+ if (!cookies) {
1745
+ return { token: null };
1746
+ }
1747
+ const cookieName = this.config.rememberCookieName || "remember_token";
1748
+ const token = cookies[cookieName];
1749
+ return { token: token || null };
1750
+ }
1751
+ async regenerateSession() {
1752
+ const { auth } = this.req.session;
1753
+ return new Promise((resolve, reject) => {
1754
+ this.req.session.regenerate((err) => {
1755
+ if (err) {
1756
+ reject(err);
1757
+ return;
1758
+ }
1759
+ this.req.session.auth = auth;
1760
+ resolve();
1761
+ });
1762
+ });
1763
+ }
1764
+ async resyncSession(force = false) {
1765
+ if (!this.isLoggedIn()) {
1766
+ return;
1767
+ }
1768
+ if (this.req.session.auth.shouldForceLogout) {
1769
+ await this.logout();
1770
+ return;
1771
+ }
1772
+ const interval = (0, import_ms3.default)(this.config.resyncInterval || "30s");
1773
+ const lastResync = new Date(this.req.session.auth.lastResync);
1774
+ if (!force && lastResync && lastResync.getTime() > Date.now() - interval) {
1775
+ return;
1776
+ }
1777
+ const account = await this.getAuthAccount();
1778
+ if (!account) {
1779
+ await this.logout();
1780
+ return;
1781
+ }
1782
+ if (account.force_logout > this.req.session.auth.forceLogout) {
1783
+ await this.logout();
1784
+ return;
1785
+ }
1786
+ this.req.session.auth.shouldForceLogout = false;
1787
+ this.req.session.auth.email = account.email;
1788
+ this.req.session.auth.status = account.status;
1789
+ this.req.session.auth.rolemask = account.rolemask;
1790
+ this.req.session.auth.verified = account.verified;
1791
+ this.req.session.auth.lastResync = /* @__PURE__ */ new Date();
1792
+ }
1793
+ async processRememberDirective() {
1794
+ if (this.isLoggedIn()) {
1795
+ return;
1796
+ }
1797
+ const { token } = this.getRememberToken();
1798
+ if (!token) {
1799
+ return;
1800
+ }
1801
+ const remember = await this.queries.findRememberToken(token);
1802
+ if (!remember) {
1803
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1804
+ return;
1805
+ }
1806
+ if (/* @__PURE__ */ new Date() > remember.expires) {
1807
+ await this.queries.deleteRememberToken(token);
1808
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1809
+ return;
1810
+ }
1811
+ await this.queries.deleteExpiredRememberTokensForAccount(remember.account_id);
1812
+ const account = await this.queries.findAccountById(remember.account_id);
1813
+ if (!account) {
1814
+ await this.queries.deleteRememberToken(token);
1815
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1816
+ return;
1817
+ }
1818
+ await this.onLoginSuccessful(account, true);
1819
+ }
1820
+ async onLoginSuccessful(account, remember = false) {
1821
+ await this.queries.updateAccountLastLogin(account.id);
1822
+ return new Promise((resolve, reject) => {
1823
+ if (!this.req.session?.regenerate) {
1824
+ resolve();
1825
+ return;
1826
+ }
1827
+ this.req.session.regenerate(async (err) => {
1828
+ if (err) {
1829
+ reject(err);
1830
+ return;
1831
+ }
1832
+ const session = {
1833
+ loggedIn: true,
1834
+ accountId: account.id,
1835
+ userId: account.user_id,
1836
+ email: account.email,
1837
+ status: account.status,
1838
+ rolemask: account.rolemask,
1839
+ remembered: remember,
1840
+ lastResync: /* @__PURE__ */ new Date(),
1841
+ lastRememberCheck: /* @__PURE__ */ new Date(),
1842
+ forceLogout: account.force_logout,
1843
+ verified: account.verified,
1844
+ shouldForceLogout: false
1845
+ };
1846
+ this.req.session.auth = session;
1847
+ if (remember) {
1848
+ await this.createRememberDirective(account);
1849
+ }
1850
+ this.req.session.save((err2) => {
1851
+ if (err2) {
1852
+ reject(err2);
1853
+ return;
1854
+ }
1855
+ resolve();
1856
+ });
1857
+ });
1858
+ });
1859
+ }
1860
+ async createRememberDirective(account) {
1861
+ const token = import_hash4.hash.encode(account.email);
1862
+ const duration = this.config.rememberDuration || "30d";
1863
+ const expires = new Date(Date.now() + (0, import_ms3.default)(duration));
1864
+ await this.queries.createRememberToken({
1865
+ accountId: account.id,
1866
+ token,
1867
+ expires
1868
+ });
1869
+ this.setRememberCookie(token, expires);
1870
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.RememberTokenCreated, this.req, true, { email: account.email, duration });
1871
+ return token;
1872
+ }
1873
+ /**
1874
+ * Check if the current user is logged in.
1875
+ * @returns true if user has an active authenticated session
1876
+ */
1877
+ isLoggedIn() {
1878
+ return this.req.session?.auth?.loggedIn ?? false;
1879
+ }
1880
+ /**
1881
+ * Authenticate user with email and password.
1882
+ * Creates a new session and optionally sets a remember token for persistent login.
1883
+ *
1884
+ * @param email - User's email address
1885
+ * @param password - Plain text password
1886
+ * @param remember - If true, sets a persistent cookie for auto-login on future visits
1887
+ * @throws {UserNotFoundError} Account with this email doesn't exist
1888
+ * @throws {InvalidPasswordError} Password is incorrect
1889
+ * @throws {EmailNotVerifiedError} Account exists but email is not verified
1890
+ * @throws {UserInactiveError} Account is banned, locked, or otherwise inactive
1891
+ */
1892
+ async login(email, password, remember = false) {
1893
+ try {
1894
+ const account = await this.queries.findAccountByEmail(email);
1895
+ if (!account) {
1896
+ await this.activityLogger.logActivity(null, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "account_not_found" });
1897
+ throw new UserNotFoundError();
1898
+ }
1899
+ if (!account.password || !import_hash4.hash.verify(account.password, password)) {
1900
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "invalid_password" });
1901
+ throw new InvalidPasswordError();
1902
+ }
1903
+ if (!account.verified) {
1904
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "email_not_verified" });
1905
+ throw new EmailNotVerifiedError();
1906
+ }
1907
+ if (account.status !== AuthStatus.Normal) {
1908
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.FailedLogin, this.req, false, { email, reason: "account_inactive", status: account.status });
1909
+ throw new UserInactiveError();
1910
+ }
1911
+ if (this.config.twoFactor?.enabled && await this.shouldRequire2FA(account)) {
1912
+ const twoFactorMethods = await this.queries.findTwoFactorMethodsByAccountId(account.id);
1913
+ const enabledMethods = twoFactorMethods.filter((method) => method.verified);
1914
+ if (enabledMethods.length > 0) {
1915
+ const challenge = await this.twoFactor.createChallenge(account.id);
1916
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
1917
+ const expiresAt = new Date(Date.now() + (0, import_ms3.default)(expiryDuration));
1918
+ this.req.session.auth = {
1919
+ loggedIn: false,
1920
+ accountId: 0,
1921
+ userId: "",
1922
+ email: "",
1923
+ status: 0,
1924
+ rolemask: 0,
1925
+ remembered: false,
1926
+ lastResync: /* @__PURE__ */ new Date(),
1927
+ lastRememberCheck: /* @__PURE__ */ new Date(),
1928
+ forceLogout: 0,
1929
+ verified: false,
1930
+ awaitingTwoFactor: {
1931
+ accountId: account.id,
1932
+ expiresAt,
1933
+ remember,
1934
+ availableMechanisms: enabledMethods.map((m) => m.mechanism),
1935
+ attemptedMechanisms: [],
1936
+ originalEmail: account.email,
1937
+ selectors: challenge.selectors
1938
+ }
1939
+ };
1940
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.TwoFactorFailed, this.req, true, { prompt: true, mechanisms: enabledMethods.map((m) => m.mechanism) });
1941
+ throw new SecondFactorRequiredError(challenge);
1942
+ }
1943
+ }
1944
+ await this.onLoginSuccessful(account, remember);
1945
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Login, this.req, true, { email, remember });
1946
+ } catch (error) {
1947
+ throw error;
1948
+ }
1949
+ }
1950
+ /**
1951
+ * Complete two-factor authentication and log in the user.
1952
+ * This should be called after receiving a SecondFactorRequiredError.
1953
+ */
1954
+ async completeTwoFactorLogin() {
1955
+ const twoFactorState = this.req.session?.auth?.awaitingTwoFactor;
1956
+ if (!twoFactorState) {
1957
+ throw new TwoFactorExpiredError();
1958
+ }
1959
+ if (twoFactorState.expiresAt <= /* @__PURE__ */ new Date()) {
1960
+ delete this.req.session.auth.awaitingTwoFactor;
1961
+ throw new TwoFactorExpiredError();
1962
+ }
1963
+ const account = await this.queries.findAccountById(twoFactorState.accountId);
1964
+ if (!account) {
1965
+ delete this.req.session.auth.awaitingTwoFactor;
1966
+ throw new UserNotFoundError();
1967
+ }
1968
+ await this.onLoginSuccessful(account, twoFactorState.remember);
1969
+ delete this.req.session.auth.awaitingTwoFactor;
1970
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Login, this.req, true, { email: account.email, remember: twoFactorState.remember, twoFactorCompleted: true });
1971
+ }
1972
+ /**
1973
+ * Log out the current user.
1974
+ * Clears the session and removes any remember tokens.
1975
+ */
1976
+ async logout() {
1977
+ if (!this.isLoggedIn()) {
1978
+ return;
1979
+ }
1980
+ const accountId = this.getId();
1981
+ const email = this.getEmail();
1982
+ const { token } = this.getRememberToken();
1983
+ if (token) {
1984
+ await this.queries.deleteRememberToken(token);
1985
+ this.setRememberCookie(null, /* @__PURE__ */ new Date(0));
1986
+ }
1987
+ this.req.session.auth = void 0;
1988
+ if (accountId && email) {
1989
+ await this.activityLogger.logActivity(accountId, AuthActivityAction.Logout, this.req, true, { email });
1990
+ }
1991
+ }
1992
+ /**
1993
+ * Register a new account.
1994
+ *
1995
+ * @param email - Email address for the new account
1996
+ * @param password - Plain text password (will be hashed)
1997
+ * @param userId - Optional user ID to link this auth account to. If not provided, a UUID will be generated automatically.
1998
+ * @param callback - If provided, account is created unverified and callback receives confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler. If omitted, account is immediately verified.
1999
+ * @returns The created account record
2000
+ * @throws {EmailTakenError} Email is already registered
2001
+ * @throws {InvalidPasswordError} Password doesn't meet length requirements
2002
+ */
2003
+ async register(email, password, userId, callback) {
2004
+ validateEmail(email);
2005
+ this.validatePassword(password);
2006
+ const existing = await this.queries.findAccountByEmail(email);
2007
+ if (existing) {
2008
+ throw new EmailTakenError();
2009
+ }
2010
+ const finalUserId = userId || this.generateAutoUserId();
2011
+ const hashedPassword = import_hash4.hash.encode(password);
2012
+ const verified = typeof callback !== "function";
2013
+ const account = await this.queries.createAccount({
2014
+ userId: finalUserId,
2015
+ email,
2016
+ password: hashedPassword,
2017
+ verified,
2018
+ status: AuthStatus.Normal,
2019
+ rolemask: 0
2020
+ });
2021
+ if (!verified && callback) {
2022
+ await this.createConfirmationToken(account, email, callback);
2023
+ }
2024
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.Register, this.req, true, { email, verified, userId: finalUserId });
2025
+ return account;
2026
+ }
2027
+ async createConfirmationToken(account, email, callback) {
2028
+ const token = import_hash4.hash.encode(email);
2029
+ const expires = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 7);
2030
+ await this.queries.createConfirmation({
2031
+ accountId: account.id,
2032
+ token,
2033
+ email,
2034
+ expires
2035
+ });
2036
+ if (callback) {
2037
+ callback(token);
2038
+ }
2039
+ }
2040
+ /**
2041
+ * Get the current user's account ID.
2042
+ * @returns Account ID if logged in, null otherwise
2043
+ */
2044
+ getId() {
2045
+ return this.req.session?.auth?.accountId || null;
2046
+ }
2047
+ /**
2048
+ * Get the current user's email address.
2049
+ * @returns Email if logged in, null otherwise
2050
+ */
2051
+ getEmail() {
2052
+ return this.req.session?.auth?.email || null;
2053
+ }
2054
+ /**
2055
+ * Get the current user's account status.
2056
+ * @returns Status number (0=Normal, 1=Archived, 2=Banned, etc.) if logged in, null otherwise
2057
+ */
2058
+ getStatus() {
2059
+ return this.req.session?.auth?.status ?? null;
2060
+ }
2061
+ /**
2062
+ * Check if the current user's email is verified.
2063
+ * @returns true if verified, false if unverified, null if not logged in
2064
+ */
2065
+ getVerified() {
2066
+ return this.req.session?.auth?.verified ?? null;
2067
+ }
2068
+ /**
2069
+ * Get human-readable role names for the current user or a specific rolemask.
2070
+ * @param rolemask - Optional specific rolemask to check. If omitted, uses current user's roles
2071
+ * @returns Array of role names (e.g., ['Admin', 'Editor'])
2072
+ */
2073
+ getRoleNames(rolemask) {
2074
+ const mask = rolemask !== void 0 ? rolemask : this.req.session?.auth?.rolemask ?? 0;
2075
+ if (!mask && mask !== 0) {
2076
+ return [];
2077
+ }
2078
+ return Object.entries(this.getRoleMap()).filter(([key]) => mask & parseInt(key)).map(([, value]) => value);
2079
+ }
2080
+ /**
2081
+ * Get human-readable status name for the current user.
2082
+ * @returns Status name (e.g., 'Normal', 'Banned', 'Locked') if logged in, null otherwise
2083
+ */
2084
+ getStatusName() {
2085
+ const status = this.getStatus();
2086
+ if (status === null) return null;
2087
+ return this.getStatusMap()[status] || null;
2088
+ }
2089
+ /**
2090
+ * Check if the current user has a specific role.
2091
+ * @param role - Role bitmask to check (e.g., AuthRole.Admin)
2092
+ * @returns true if user has the role, false otherwise
2093
+ */
2094
+ async hasRole(role) {
2095
+ if (this.req.session?.auth) {
2096
+ return (this.req.session.auth.rolemask & role) === role;
2097
+ }
2098
+ const account = await this.getAuthAccount();
2099
+ return account ? (account.rolemask & role) === role : false;
2100
+ }
2101
+ /**
2102
+ * Check if the current user has admin privileges.
2103
+ * @returns true if user has Admin role, false otherwise
2104
+ */
2105
+ async isAdmin() {
2106
+ return this.hasRole(AuthRole.Admin);
2107
+ }
2108
+ /**
2109
+ * Check if the current user was automatically logged in via remember token.
2110
+ * @returns true if auto-logged in from persistent cookie, false if manual login or not logged in
2111
+ */
2112
+ isRemembered() {
2113
+ return this.req.session?.auth?.remembered ?? false;
2114
+ }
2115
+ /**
2116
+ * Request an email change for the current user.
2117
+ * Sends a confirmation token to verify the new email before changing it.
2118
+ *
2119
+ * @param newEmail - New email address
2120
+ * @param callback - Called with confirmation token. Create a URL like /confirm/{token} and call confirmEmail() in that handler
2121
+ * @throws {UserNotLoggedInError} User is not logged in
2122
+ * @throws {EmailTakenError} New email is already registered
2123
+ * @throws {UserNotFoundError} Current user account not found
2124
+ * @throws {EmailNotVerifiedError} Current account's email is not verified
2125
+ */
2126
+ async changeEmail(newEmail, callback) {
2127
+ if (!this.isLoggedIn()) {
2128
+ throw new UserNotLoggedInError();
2129
+ }
2130
+ validateEmail(newEmail);
2131
+ const existing = await this.queries.findAccountByEmail(newEmail);
2132
+ if (existing) {
2133
+ throw new EmailTakenError();
2134
+ }
2135
+ const account = await this.getAuthAccount();
2136
+ if (!account) {
2137
+ throw new UserNotFoundError();
2138
+ }
2139
+ if (!account.verified) {
2140
+ throw new EmailNotVerifiedError();
2141
+ }
2142
+ await this.createConfirmationToken(account, newEmail, callback);
2143
+ }
2144
+ /**
2145
+ * Confirm an email address using a token from registration or email change.
2146
+ * Updates the account to verified status and changes email if this was from changeEmail.
2147
+ *
2148
+ * @param token - Confirmation token from registration or email change
2149
+ * @returns The confirmed email address
2150
+ * @throws {ConfirmationNotFoundError} Token is invalid or doesn't exist
2151
+ * @throws {ConfirmationExpiredError} Token has expired
2152
+ * @throws {InvalidTokenError} Token format is invalid
2153
+ */
2154
+ async confirmEmail(token) {
2155
+ const confirmation = await this.queries.findConfirmation(token);
2156
+ if (!confirmation) {
2157
+ throw new ConfirmationNotFoundError();
2158
+ }
2159
+ if (new Date(confirmation.expires) < /* @__PURE__ */ new Date()) {
2160
+ throw new ConfirmationExpiredError();
2161
+ }
2162
+ if (!import_hash4.hash.verify(token, confirmation.email)) {
2163
+ throw new InvalidTokenError();
2164
+ }
2165
+ await this.queries.updateAccount(confirmation.account_id, {
2166
+ verified: true,
2167
+ email: confirmation.email
2168
+ });
2169
+ if (this.isLoggedIn() && this.req.session?.auth?.accountId === confirmation.account_id) {
2170
+ this.req.session.auth.verified = true;
2171
+ this.req.session.auth.email = confirmation.email;
2172
+ }
2173
+ await this.queries.deleteConfirmation(token);
2174
+ await this.activityLogger.logActivity(confirmation.account_id, AuthActivityAction.EmailConfirmed, this.req, true, { email: confirmation.email });
2175
+ return confirmation.email;
2176
+ }
2177
+ /**
2178
+ * Confirm email and automatically log in the user.
2179
+ * Useful for "click to verify and login" flows.
2180
+ *
2181
+ * @param token - Confirmation token from registration
2182
+ * @param remember - Whether to set persistent login cookie
2183
+ * @throws {ConfirmationNotFoundError} Token is invalid or doesn't exist
2184
+ * @throws {ConfirmationExpiredError} Token has expired
2185
+ * @throws {InvalidTokenError} Token format is invalid
2186
+ * @throws {UserNotFoundError} Associated account no longer exists
2187
+ */
2188
+ async confirmEmailAndLogin(token, remember = false) {
2189
+ const email = await this.confirmEmail(token);
2190
+ if (this.isLoggedIn()) {
2191
+ return;
2192
+ }
2193
+ const account = await this.queries.findAccountByEmail(email);
2194
+ if (!account) {
2195
+ throw new UserNotFoundError();
2196
+ }
2197
+ if (this.config.twoFactor?.enabled && await this.shouldRequire2FA(account)) {
2198
+ const twoFactorMethods = await this.queries.findTwoFactorMethodsByAccountId(account.id);
2199
+ const enabledMethods = twoFactorMethods.filter((method) => method.verified);
2200
+ if (enabledMethods.length > 0) {
2201
+ const challenge = await this.twoFactor.createChallenge(account.id);
2202
+ const expiryDuration = this.config.twoFactor?.tokenExpiry || "5m";
2203
+ const expiresAt = new Date(Date.now() + (0, import_ms3.default)(expiryDuration));
2204
+ this.req.session.auth = {
2205
+ loggedIn: false,
2206
+ accountId: 0,
2207
+ userId: "",
2208
+ email: "",
2209
+ status: 0,
2210
+ rolemask: 0,
2211
+ remembered: false,
2212
+ lastResync: /* @__PURE__ */ new Date(),
2213
+ lastRememberCheck: /* @__PURE__ */ new Date(),
2214
+ forceLogout: 0,
2215
+ verified: false,
2216
+ awaitingTwoFactor: {
2217
+ accountId: account.id,
2218
+ expiresAt,
2219
+ remember,
2220
+ availableMechanisms: enabledMethods.map((m) => m.mechanism),
2221
+ attemptedMechanisms: [],
2222
+ originalEmail: account.email,
2223
+ selectors: challenge.selectors
2224
+ }
2225
+ };
2226
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.TwoFactorFailed, this.req, true, { prompt: true, mechanisms: enabledMethods.map((m) => m.mechanism) });
2227
+ throw new SecondFactorRequiredError(challenge);
2228
+ }
2229
+ }
2230
+ await this.onLoginSuccessful(account, remember);
2231
+ }
2232
+ /**
2233
+ * Initiate a password reset for a user.
2234
+ * Creates a reset token and calls the callback to send reset email.
2235
+ *
2236
+ * @param email - Email address of account to reset
2237
+ * @param expiresAfter - Token expiration (default: 6h). Accepts ms format like '1h', '30m'
2238
+ * @param maxOpenRequests - Maximum concurrent reset tokens (default: 2)
2239
+ * @param callback - Called with reset token. Create a URL like /reset/{token} and call confirmResetPassword() in that handler
2240
+ * @throws {EmailNotVerifiedError} Account doesn't exist or email not verified
2241
+ * @throws {ResetDisabledError} Account has password reset disabled
2242
+ * @throws {TooManyResetsError} Too many active reset requests
2243
+ */
2244
+ async resetPassword(email, expiresAfter = null, maxOpenRequests = null, callback) {
2245
+ validateEmail(email);
2246
+ const expiry = !expiresAfter ? (0, import_ms3.default)("6h") : (0, import_ms3.default)(expiresAfter);
2247
+ const maxRequests = maxOpenRequests === null ? 2 : Math.max(1, maxOpenRequests);
2248
+ const account = await this.queries.findAccountByEmail(email);
2249
+ if (!account || !account.verified) {
2250
+ throw new EmailNotVerifiedError();
2251
+ }
2252
+ if (!account.resettable) {
2253
+ throw new ResetDisabledError();
2254
+ }
2255
+ const openRequests = await this.queries.countActiveResetTokensForAccount(account.id);
2256
+ if (openRequests >= maxRequests) {
2257
+ throw new TooManyResetsError();
2258
+ }
2259
+ const token = import_hash4.hash.encode(email);
2260
+ const expires = new Date(Date.now() + expiry);
2261
+ await this.queries.createResetToken({
2262
+ accountId: account.id,
2263
+ token,
2264
+ expires
2265
+ });
2266
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.PasswordResetRequested, this.req, true, { email });
2267
+ if (callback) {
2268
+ callback(token);
2269
+ }
2270
+ }
2271
+ /**
2272
+ * Complete a password reset using a reset token.
2273
+ * Changes the password and optionally logs out all sessions.
2274
+ *
2275
+ * @param token - Reset token from resetPassword callback
2276
+ * @param password - New password (will be hashed)
2277
+ * @param logout - Whether to force logout all sessions (default: true)
2278
+ * @throws {ResetNotFoundError} Token is invalid or doesn't exist
2279
+ * @throws {ResetExpiredError} Token has expired
2280
+ * @throws {UserNotFoundError} Associated account no longer exists
2281
+ * @throws {ResetDisabledError} Account has password reset disabled
2282
+ * @throws {InvalidPasswordError} New password doesn't meet requirements
2283
+ * @throws {InvalidTokenError} Token format is invalid
2284
+ */
2285
+ async confirmResetPassword(token, password, logout = true) {
2286
+ const reset = await this.queries.findResetToken(token);
2287
+ if (!reset) {
2288
+ throw new ResetNotFoundError();
2289
+ }
2290
+ if (new Date(reset.expires) < /* @__PURE__ */ new Date()) {
2291
+ throw new ResetExpiredError();
2292
+ }
2293
+ const account = await this.queries.findAccountById(reset.account_id);
2294
+ if (!account) {
2295
+ throw new UserNotFoundError();
2296
+ }
2297
+ if (!account.resettable) {
2298
+ throw new ResetDisabledError();
2299
+ }
2300
+ this.validatePassword(password);
2301
+ if (!import_hash4.hash.verify(token, account.email)) {
2302
+ throw new InvalidTokenError();
2303
+ }
2304
+ await this.queries.updateAccount(account.id, {
2305
+ password: import_hash4.hash.encode(password)
2306
+ });
2307
+ if (logout) {
2308
+ await this.forceLogoutForAccountById(account.id);
2309
+ }
2310
+ await this.queries.deleteResetToken(token);
2311
+ await this.activityLogger.logActivity(account.id, AuthActivityAction.PasswordResetCompleted, this.req, true, { email: account.email });
2312
+ }
2313
+ /**
2314
+ * Verify if a password matches the current user's password.
2315
+ * Useful for "confirm current password" flows before sensitive operations.
2316
+ *
2317
+ * @param password - Password to verify
2318
+ * @returns true if password matches, false otherwise
2319
+ * @throws {UserNotLoggedInError} User is not logged in
2320
+ * @throws {UserNotFoundError} Current user account not found
2321
+ */
2322
+ async verifyPassword(password) {
2323
+ if (!this.isLoggedIn()) {
2324
+ throw new UserNotLoggedInError();
2325
+ }
2326
+ const account = await this.getAuthAccount();
2327
+ if (!account) {
2328
+ throw new UserNotFoundError();
2329
+ }
2330
+ if (!account.password) {
2331
+ return false;
2332
+ }
2333
+ return import_hash4.hash.verify(account.password, password);
2334
+ }
2335
+ async forceLogoutForAccountById(accountId) {
2336
+ await this.queries.deleteRememberTokensForAccount(accountId);
2337
+ await this.queries.incrementForceLogout(accountId);
2338
+ }
2339
+ /**
2340
+ * Force logout all OTHER sessions while keeping current session active.
2341
+ * Useful for "logout other devices" functionality.
2342
+ */
2343
+ async logoutEverywhereElse() {
2344
+ if (!this.isLoggedIn()) {
2345
+ return;
2346
+ }
2347
+ const accountId = this.getId();
2348
+ if (!accountId) {
2349
+ return;
2350
+ }
2351
+ const account = await this.queries.findAccountById(accountId);
2352
+ if (!account) {
2353
+ await this.logout();
2354
+ return;
2355
+ }
2356
+ await this.forceLogoutForAccountById(accountId);
2357
+ this.req.session.auth.forceLogout += 1;
2358
+ await this.regenerateSession();
2359
+ }
2360
+ /**
2361
+ * Force logout ALL sessions including the current one.
2362
+ * Logs out everywhere else, then logs out current session.
2363
+ */
2364
+ async logoutEverywhere() {
2365
+ if (!this.isLoggedIn()) {
2366
+ return;
2367
+ }
2368
+ await this.logoutEverywhereElse();
2369
+ await this.logout();
2370
+ }
2371
+ };
2372
+
2373
+ // src/middleware.ts
2374
+ function createAuthMiddleware(config) {
2375
+ return async (req, res, next) => {
2376
+ try {
2377
+ const authManager = new AuthManager(req, res, config);
2378
+ const authAdminManager = new AuthAdminManager(req, res, config, authManager);
2379
+ req.auth = authManager;
2380
+ req.authAdmin = authAdminManager;
2381
+ await authManager.resyncSession();
2382
+ await authManager.processRememberDirective();
2383
+ next();
2384
+ } catch (error) {
2385
+ next(error);
2386
+ }
2387
+ };
2388
+ }
2389
+
2390
+ // src/schema.ts
2391
+ async function createAuthTables(config) {
2392
+ const prefix = config.tablePrefix || "user_";
2393
+ const { db } = config;
2394
+ const accountsTable = `${prefix}accounts`;
2395
+ await db.query(`
2396
+ CREATE TABLE IF NOT EXISTS ${accountsTable} (
2397
+ id SERIAL PRIMARY KEY,
2398
+ user_id VARCHAR(255) NOT NULL,
2399
+ email VARCHAR(255) NOT NULL UNIQUE,
2400
+ password VARCHAR(255),
2401
+ verified BOOLEAN DEFAULT FALSE,
2402
+ status INTEGER DEFAULT 0,
2403
+ rolemask INTEGER DEFAULT 0,
2404
+ last_login TIMESTAMPTZ,
2405
+ force_logout INTEGER DEFAULT 0,
2406
+ resettable BOOLEAN DEFAULT TRUE,
2407
+ registered TIMESTAMPTZ DEFAULT NOW(),
2408
+ CONSTRAINT ${prefix}unique_user_id_per_account UNIQUE(user_id)
2409
+ )
2410
+ `);
2411
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_user_id ON ${accountsTable}(user_id)`);
2412
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_email ON ${accountsTable}(email)`);
2413
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}accounts_status ON ${accountsTable}(status)`);
2414
+ const confirmationsTable = `${prefix}confirmations`;
2415
+ await db.query(`
2416
+ CREATE TABLE IF NOT EXISTS ${confirmationsTable} (
2417
+ id SERIAL PRIMARY KEY,
2418
+ account_id INTEGER NOT NULL,
2419
+ token VARCHAR(255) NOT NULL,
2420
+ email VARCHAR(255) NOT NULL,
2421
+ expires TIMESTAMPTZ NOT NULL,
2422
+ CONSTRAINT fk_${prefix}confirmations_account
2423
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2424
+ )
2425
+ `);
2426
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_token ON ${confirmationsTable}(token)`);
2427
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_email ON ${confirmationsTable}(email)`);
2428
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_account_id ON ${confirmationsTable}(account_id)`);
2429
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}confirmations_expires ON ${confirmationsTable}(expires)`);
2430
+ const remembersTable = `${prefix}remembers`;
2431
+ await db.query(`
2432
+ CREATE TABLE IF NOT EXISTS ${remembersTable} (
2433
+ id SERIAL PRIMARY KEY,
2434
+ account_id INTEGER NOT NULL,
2435
+ token VARCHAR(255) NOT NULL,
2436
+ expires TIMESTAMPTZ NOT NULL,
2437
+ CONSTRAINT fk_${prefix}remembers_account
2438
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2439
+ )
2440
+ `);
2441
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_token ON ${remembersTable}(token)`);
2442
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_account_id ON ${remembersTable}(account_id)`);
2443
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}remembers_expires ON ${remembersTable}(expires)`);
2444
+ const resetsTable = `${prefix}resets`;
2445
+ await db.query(`
2446
+ CREATE TABLE IF NOT EXISTS ${resetsTable} (
2447
+ id SERIAL PRIMARY KEY,
2448
+ account_id INTEGER NOT NULL,
2449
+ token VARCHAR(255) NOT NULL,
2450
+ expires TIMESTAMPTZ NOT NULL,
2451
+ CONSTRAINT fk_${prefix}resets_account
2452
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2453
+ )
2454
+ `);
2455
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_token ON ${resetsTable}(token)`);
2456
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_account_id ON ${resetsTable}(account_id)`);
2457
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}resets_expires ON ${resetsTable}(expires)`);
2458
+ const providersTable = `${prefix}providers`;
2459
+ await db.query(`
2460
+ CREATE TABLE IF NOT EXISTS ${providersTable} (
2461
+ id SERIAL PRIMARY KEY,
2462
+ account_id INTEGER NOT NULL,
2463
+ provider VARCHAR(50) NOT NULL,
2464
+ provider_id VARCHAR(255) NOT NULL,
2465
+ provider_email VARCHAR(255),
2466
+ provider_username VARCHAR(255),
2467
+ provider_name VARCHAR(255),
2468
+ provider_avatar VARCHAR(500),
2469
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2470
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
2471
+ CONSTRAINT fk_${prefix}providers_account
2472
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE,
2473
+ CONSTRAINT ${prefix}unique_provider_identity
2474
+ UNIQUE(provider, provider_id)
2475
+ )
2476
+ `);
2477
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_account_id ON ${providersTable}(account_id)`);
2478
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_provider ON ${providersTable}(provider)`);
2479
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_provider_id ON ${providersTable}(provider_id)`);
2480
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}providers_email ON ${providersTable}(provider_email)`);
2481
+ const activityTable = `${prefix}activity_log`;
2482
+ await db.query(`
2483
+ CREATE TABLE IF NOT EXISTS ${activityTable} (
2484
+ id SERIAL PRIMARY KEY,
2485
+ account_id INTEGER,
2486
+ action VARCHAR(255) NOT NULL,
2487
+ ip_address INET,
2488
+ user_agent TEXT,
2489
+ browser VARCHAR(255),
2490
+ os VARCHAR(255),
2491
+ device VARCHAR(255),
2492
+ success BOOLEAN DEFAULT TRUE,
2493
+ metadata JSONB,
2494
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2495
+ CONSTRAINT fk_${prefix}activity_log_account
2496
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2497
+ )
2498
+ `);
2499
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_created_at ON ${activityTable}(created_at DESC)`);
2500
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_account_id ON ${activityTable}(account_id)`);
2501
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}activity_log_action ON ${activityTable}(action)`);
2502
+ const twoFactorMethodsTable = `${prefix}2fa_methods`;
2503
+ await db.query(`
2504
+ CREATE TABLE IF NOT EXISTS ${twoFactorMethodsTable} (
2505
+ id SERIAL PRIMARY KEY,
2506
+ account_id INTEGER NOT NULL,
2507
+ mechanism INTEGER NOT NULL,
2508
+ secret VARCHAR(255),
2509
+ backup_codes TEXT[],
2510
+ verified BOOLEAN DEFAULT FALSE,
2511
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2512
+ last_used_at TIMESTAMPTZ,
2513
+ CONSTRAINT fk_${prefix}2fa_methods_account
2514
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE,
2515
+ CONSTRAINT ${prefix}unique_account_mechanism
2516
+ UNIQUE(account_id, mechanism)
2517
+ )
2518
+ `);
2519
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_methods_account_id ON ${twoFactorMethodsTable}(account_id)`);
2520
+ const twoFactorTokensTable = `${prefix}2fa_tokens`;
2521
+ await db.query(`
2522
+ CREATE TABLE IF NOT EXISTS ${twoFactorTokensTable} (
2523
+ id SERIAL PRIMARY KEY,
2524
+ account_id INTEGER NOT NULL,
2525
+ mechanism INTEGER NOT NULL,
2526
+ selector VARCHAR(32) NOT NULL,
2527
+ token_hash VARCHAR(255) NOT NULL,
2528
+ expires_at TIMESTAMPTZ NOT NULL,
2529
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2530
+ CONSTRAINT fk_${prefix}2fa_tokens_account
2531
+ FOREIGN KEY (account_id) REFERENCES ${accountsTable}(id) ON DELETE CASCADE
2532
+ )
2533
+ `);
2534
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_selector ON ${twoFactorTokensTable}(selector)`);
2535
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_account_id ON ${twoFactorTokensTable}(account_id)`);
2536
+ await db.query(`CREATE INDEX IF NOT EXISTS idx_${prefix}2fa_tokens_expires ON ${twoFactorTokensTable}(expires_at)`);
2537
+ }
2538
+ async function dropAuthTables(config) {
2539
+ const prefix = config.tablePrefix || "user_";
2540
+ const { db } = config;
2541
+ await db.query(`DROP TABLE IF EXISTS ${prefix}2fa_tokens CASCADE`);
2542
+ await db.query(`DROP TABLE IF EXISTS ${prefix}2fa_methods CASCADE`);
2543
+ await db.query(`DROP TABLE IF EXISTS ${prefix}activity_log CASCADE`);
2544
+ await db.query(`DROP TABLE IF EXISTS ${prefix}providers CASCADE`);
2545
+ await db.query(`DROP TABLE IF EXISTS ${prefix}resets CASCADE`);
2546
+ await db.query(`DROP TABLE IF EXISTS ${prefix}remembers CASCADE`);
2547
+ await db.query(`DROP TABLE IF EXISTS ${prefix}confirmations CASCADE`);
2548
+ await db.query(`DROP TABLE IF EXISTS ${prefix}accounts CASCADE`);
2549
+ }
2550
+ async function cleanupExpiredTokens(config) {
2551
+ const prefix = config.tablePrefix || "user_";
2552
+ const { db } = config;
2553
+ await db.query(`DELETE FROM ${prefix}confirmations WHERE expires < NOW()`);
2554
+ await db.query(`DELETE FROM ${prefix}remembers WHERE expires < NOW()`);
2555
+ await db.query(`DELETE FROM ${prefix}resets WHERE expires < NOW()`);
2556
+ await db.query(`DELETE FROM ${prefix}2fa_tokens WHERE expires_at < NOW()`);
2557
+ }
2558
+ async function getAuthTableStats(config) {
2559
+ const prefix = config.tablePrefix || "user_";
2560
+ const { db } = config;
2561
+ const [accountsResult, providersResult, confirmationsResult, remembersResult, resetsResult, twoFactorMethodsResult, twoFactorTokensResult, expiredConfirmationsResult, expiredRemembersResult, expiredResetsResult, expiredTwoFactorTokensResult] = await Promise.all([
2562
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}accounts`),
2563
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}providers`),
2564
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}confirmations`),
2565
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}remembers`),
2566
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}resets`),
2567
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_methods`),
2568
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_tokens`),
2569
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}confirmations WHERE expires < NOW()`),
2570
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}remembers WHERE expires < NOW()`),
2571
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}resets WHERE expires < NOW()`),
2572
+ db.query(`SELECT COUNT(*) as count FROM ${prefix}2fa_tokens WHERE expires_at < NOW()`)
2573
+ ]);
2574
+ return {
2575
+ accounts: parseInt(accountsResult.rows[0]?.count || "0"),
2576
+ providers: parseInt(providersResult.rows[0]?.count || "0"),
2577
+ confirmations: parseInt(confirmationsResult.rows[0]?.count || "0"),
2578
+ remembers: parseInt(remembersResult.rows[0]?.count || "0"),
2579
+ resets: parseInt(resetsResult.rows[0]?.count || "0"),
2580
+ twoFactorMethods: parseInt(twoFactorMethodsResult.rows[0]?.count || "0"),
2581
+ twoFactorTokens: parseInt(twoFactorTokensResult.rows[0]?.count || "0"),
2582
+ expiredConfirmations: parseInt(expiredConfirmationsResult.rows[0]?.count || "0"),
2583
+ expiredRemembers: parseInt(expiredRemembersResult.rows[0]?.count || "0"),
2584
+ expiredResets: parseInt(expiredResetsResult.rows[0]?.count || "0"),
2585
+ expiredTwoFactorTokens: parseInt(expiredTwoFactorTokensResult.rows[0]?.count || "0")
2586
+ };
2587
+ }
2588
+ // Annotate the CommonJS export names for ESM import in node:
2589
+ 0 && (module.exports = {
2590
+ ActivityLogger,
2591
+ AuthActivityAction,
2592
+ AuthError,
2593
+ AuthRole,
2594
+ AuthStatus,
2595
+ AzureProvider,
2596
+ BaseOAuthProvider,
2597
+ ConfirmationExpiredError,
2598
+ ConfirmationNotFoundError,
2599
+ EmailNotVerifiedError,
2600
+ EmailTakenError,
2601
+ GitHubProvider,
2602
+ GoogleProvider,
2603
+ InvalidBackupCodeError,
2604
+ InvalidEmailError,
2605
+ InvalidPasswordError,
2606
+ InvalidTokenError,
2607
+ InvalidTwoFactorCodeError,
2608
+ OtpProvider,
2609
+ ResetDisabledError,
2610
+ ResetExpiredError,
2611
+ ResetNotFoundError,
2612
+ SecondFactorRequiredError,
2613
+ TooManyResetsError,
2614
+ TotpProvider,
2615
+ TwoFactorAlreadyEnabledError,
2616
+ TwoFactorExpiredError,
2617
+ TwoFactorManager,
2618
+ TwoFactorMechanism,
2619
+ TwoFactorNotSetupError,
2620
+ TwoFactorSetupIncompleteError,
2621
+ UserInactiveError,
2622
+ UserNotFoundError,
2623
+ UserNotLoggedInError,
2624
+ cleanupExpiredTokens,
2625
+ createAuthMiddleware,
2626
+ createAuthTables,
2627
+ dropAuthTables,
2628
+ getAuthTableStats,
2629
+ isValidEmail,
2630
+ validateEmail
2631
+ });
2632
+ //# sourceMappingURL=index.cjs.map