@luckystack/login 0.5.0 → 0.6.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/CHANGELOG.md +51 -14
- package/dist/{chunk-W6C2J7AW.js → chunk-5WBFVSMW.js} +4 -4
- package/dist/chunk-5WBFVSMW.js.map +1 -0
- package/dist/{chunk-K5Z5KKKT.js → chunk-HJHCX2JO.js} +1 -1
- package/dist/{chunk-K5Z5KKKT.js.map → chunk-HJHCX2JO.js.map} +1 -1
- package/dist/index.d.ts +234 -48
- package/dist/index.js +566 -33
- package/dist/index.js.map +1 -1
- package/dist/logout-RX77TPUH.js +7 -0
- package/dist/register.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-W6C2J7AW.js.map +0 -1
- package/dist/logout-MVBQYT4W.js +0 -7
- /package/dist/{logout-MVBQYT4W.js.map → logout-RX77TPUH.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
registerOAuthProviderFactory,
|
|
15
15
|
registerOAuthProviders,
|
|
16
16
|
registerUserAdapter
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-HJHCX2JO.js";
|
|
18
18
|
import {
|
|
19
19
|
activeUsersKeyFor,
|
|
20
20
|
deleteSession,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
revokeUserSessions,
|
|
30
30
|
saveSession,
|
|
31
31
|
sessionKeyFor
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-5WBFVSMW.js";
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
35
|
import { registerSessionProvider } from "@luckystack/core";
|
|
@@ -141,7 +141,7 @@ var registerPostLoginRedirect = (resolver) => {
|
|
|
141
141
|
var getPostLoginRedirect = () => activeResolver;
|
|
142
142
|
|
|
143
143
|
// src/login.ts
|
|
144
|
-
import { URLSearchParams } from "url";
|
|
144
|
+
import { URLSearchParams as URLSearchParams2 } from "url";
|
|
145
145
|
import { tryCatch as tryCatch2, tryCatchSync, redis as redisClient, getUploadsDir, dispatchHook, getLogger as getLogger3, formatKey, getProjectConfig as getProjectConfig4, getCookieValue } from "@luckystack/core";
|
|
146
146
|
import bcrypt from "bcryptjs";
|
|
147
147
|
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
@@ -512,7 +512,7 @@ var resolveUploadedAvatar = async (userId) => {
|
|
|
512
512
|
return avatarStat ? `${userId}.webp` : null;
|
|
513
513
|
};
|
|
514
514
|
var sanitizeUserForSession = (user) => {
|
|
515
|
-
const { password: _password, ...safeUser } = user;
|
|
515
|
+
const { password: _password, totpSecret: _totpSecret, recoveryCodes: _recoveryCodes, ...safeUser } = user;
|
|
516
516
|
return safeUser;
|
|
517
517
|
};
|
|
518
518
|
var emitLoginFailed = (payload) => {
|
|
@@ -645,6 +645,41 @@ var registerWithCredentials = async ({
|
|
|
645
645
|
session: newUser
|
|
646
646
|
};
|
|
647
647
|
};
|
|
648
|
+
var twoFactorGate = null;
|
|
649
|
+
var registerTwoFactorGate = (gate) => {
|
|
650
|
+
twoFactorGate = gate;
|
|
651
|
+
};
|
|
652
|
+
var applyTwoFactorGate = async (user, context) => twoFactorGate ? twoFactorGate(user, context) : null;
|
|
653
|
+
var finalizeLogin = async (user, options) => {
|
|
654
|
+
const newToken = randomBytes(32).toString("hex");
|
|
655
|
+
const previousLogin = user.lastLogin ?? null;
|
|
656
|
+
const nowLogin = /* @__PURE__ */ new Date();
|
|
657
|
+
await tryCatch2(() => getUserAdapter().update(user.id, { lastLogin: nowLogin }));
|
|
658
|
+
const newUser = {
|
|
659
|
+
...sanitizeUserForSession(user),
|
|
660
|
+
token: newToken,
|
|
661
|
+
lastLogin: nowLogin,
|
|
662
|
+
previousLogin
|
|
663
|
+
};
|
|
664
|
+
const uploadedAvatar = await resolveUploadedAvatar(newUser.id);
|
|
665
|
+
if (uploadedAvatar) {
|
|
666
|
+
newUser.avatar = uploadedAvatar;
|
|
667
|
+
}
|
|
668
|
+
const saved = await saveSession(newToken, newUser, true, { supersedeToken: options.supersedeToken });
|
|
669
|
+
if (!saved.ok) {
|
|
670
|
+
emitLoginFailed({ email: options.email, userId: newUser.id, provider: options.provider, reason: saved.errorCode, stage: "login", requesterIp: options.requesterIp });
|
|
671
|
+
return { status: false, reason: saved.errorCode };
|
|
672
|
+
}
|
|
673
|
+
await dispatchHook("postLogin", { userId: newUser.id, provider: options.provider, isNewUser: false, token: newToken });
|
|
674
|
+
if (options.email) {
|
|
675
|
+
void clearAuthFailures(options.email, options.requesterIp);
|
|
676
|
+
if (options.requesterIp) void clearAuthFailures(options.email);
|
|
677
|
+
}
|
|
678
|
+
if (isDevMode()) {
|
|
679
|
+
getLogger3().debug(`${options.provider} login success for user ${newUser.id}`);
|
|
680
|
+
}
|
|
681
|
+
return { status: true, reason: "login.loggedIn", newToken, session: newUser };
|
|
682
|
+
};
|
|
648
683
|
var loginWithCredentialsCore = async ({ email, password }, options) => {
|
|
649
684
|
const requesterIp = options?.requesterIp;
|
|
650
685
|
const preLoginResult = await dispatchHook("preLogin", { email, provider: "credentials" });
|
|
@@ -686,32 +721,14 @@ var loginWithCredentialsCore = async ({ email, password }, options) => {
|
|
|
686
721
|
await dispatchHook("loginFailed", { email, userId: findUserResponse.id, provider: "credentials", reason: "login.wrongPassword", stage: "login", requesterIp });
|
|
687
722
|
return { status: false, reason: "login.wrongPassword" };
|
|
688
723
|
}
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
previousLogin
|
|
698
|
-
};
|
|
699
|
-
const uploadedAvatar = await resolveUploadedAvatar(newUser.id);
|
|
700
|
-
if (uploadedAvatar) {
|
|
701
|
-
newUser.avatar = uploadedAvatar;
|
|
702
|
-
}
|
|
703
|
-
const saved = await saveSession(newToken, newUser, true, { supersedeToken: options?.supersedeToken });
|
|
704
|
-
if (!saved.ok) {
|
|
705
|
-
emitLoginFailed({ email, userId: newUser.id, provider: "credentials", reason: saved.errorCode, stage: "login", requesterIp });
|
|
706
|
-
return { status: false, reason: saved.errorCode };
|
|
707
|
-
}
|
|
708
|
-
await dispatchHook("postLogin", { userId: newUser.id, provider: "credentials", isNewUser: false, token: newToken });
|
|
709
|
-
void clearAuthFailures(email, requesterIp);
|
|
710
|
-
if (requesterIp) void clearAuthFailures(email);
|
|
711
|
-
if (isDevMode()) {
|
|
712
|
-
getLogger3().debug(`credentials login success for user ${newUser.id}`);
|
|
713
|
-
}
|
|
714
|
-
return { status: true, reason: "login.loggedIn", newToken, session: newUser };
|
|
724
|
+
const challenge = await applyTwoFactorGate(findUserResponse, { requesterIp });
|
|
725
|
+
if (challenge) return challenge;
|
|
726
|
+
return finalizeLogin(findUserResponse, {
|
|
727
|
+
provider: "credentials",
|
|
728
|
+
email,
|
|
729
|
+
supersedeToken: options?.supersedeToken,
|
|
730
|
+
requesterIp
|
|
731
|
+
});
|
|
715
732
|
};
|
|
716
733
|
var loginWithCredentials = async (params, options) => {
|
|
717
734
|
const creds = normalizeCredentials(params);
|
|
@@ -777,7 +794,7 @@ var exchangeOAuthToken = async (provider, code, codeVerifier) => {
|
|
|
777
794
|
}
|
|
778
795
|
return await response3.json();
|
|
779
796
|
}
|
|
780
|
-
const formParams = new
|
|
797
|
+
const formParams = new URLSearchParams2();
|
|
781
798
|
formParams.append("client_id", provider.clientID);
|
|
782
799
|
formParams.append("client_secret", provider.clientSecret);
|
|
783
800
|
formParams.append("code", values.code);
|
|
@@ -1020,7 +1037,7 @@ var loginCallback = async (pathname, req, _res, options = {}) => {
|
|
|
1020
1037
|
if (!providerResolution) return false;
|
|
1021
1038
|
const { provider, providerName } = providerResolution;
|
|
1022
1039
|
const queryString = (req.url ?? "").split("?")[1] ?? "";
|
|
1023
|
-
const params = new
|
|
1040
|
+
const params = new URLSearchParams2(queryString);
|
|
1024
1041
|
const code = params.get("code");
|
|
1025
1042
|
const state = params.get("state");
|
|
1026
1043
|
const cookieNonce = getCookieValue(req.headers.cookie, OAUTH_STATE_COOKIE_NAME) ?? "";
|
|
@@ -1238,6 +1255,500 @@ var sendEmailChangeConfirmation = async ({ userId, newEmail, userName, brand })
|
|
|
1238
1255
|
return result.ok ? { ok: true, token } : { ok: false, reason: result.reason, token };
|
|
1239
1256
|
};
|
|
1240
1257
|
|
|
1258
|
+
// src/twoFactor.ts
|
|
1259
|
+
import crypto3 from "crypto";
|
|
1260
|
+
import { acquireLease, dispatchHook as dispatchHook3, formatKey as formatKey5, getLogger as getLogger5, getProjectConfig as getProjectConfig9, getProjectName, redis as redis4, releaseLease, tryCatch as tryCatch3 } from "@luckystack/core";
|
|
1261
|
+
|
|
1262
|
+
// src/emailOtp.ts
|
|
1263
|
+
import crypto from "crypto";
|
|
1264
|
+
import { formatKey as formatKey4, redis as redis3 } from "@luckystack/core";
|
|
1265
|
+
var normalizeIdentity = (identity) => identity.trim().toLowerCase();
|
|
1266
|
+
var codeKey = (purpose, identity) => formatKey4("-emailcode", `${purpose}:${normalizeIdentity(identity)}`);
|
|
1267
|
+
var attemptsKey = (purpose, identity) => formatKey4("-emailcode-attempts", `${purpose}:${normalizeIdentity(identity)}`);
|
|
1268
|
+
var hashCode = (code) => crypto.createHash("sha256").update(code).digest("hex");
|
|
1269
|
+
var generateNumericCode = (digits) => String(crypto.randomInt(0, 10 ** digits)).padStart(digits, "0");
|
|
1270
|
+
var issueEmailCode = async ({ purpose, identity, ttlSeconds, digits }) => {
|
|
1271
|
+
const code = generateNumericCode(digits);
|
|
1272
|
+
await redis3.set(codeKey(purpose, identity), hashCode(code), "EX", ttlSeconds);
|
|
1273
|
+
await redis3.del(attemptsKey(purpose, identity));
|
|
1274
|
+
return code;
|
|
1275
|
+
};
|
|
1276
|
+
var verifyEmailCode = async ({ purpose, identity, code, maxAttempts }) => {
|
|
1277
|
+
const key = codeKey(purpose, identity);
|
|
1278
|
+
const storedHash = await redis3.get(key);
|
|
1279
|
+
if (!storedHash) return "expired";
|
|
1280
|
+
const counter = attemptsKey(purpose, identity);
|
|
1281
|
+
const attempts = await redis3.incr(counter);
|
|
1282
|
+
const remaining = await redis3.ttl(key);
|
|
1283
|
+
await redis3.expire(counter, remaining > 0 ? remaining : 600);
|
|
1284
|
+
if (attempts > maxAttempts) {
|
|
1285
|
+
await redis3.del(key);
|
|
1286
|
+
await redis3.del(counter);
|
|
1287
|
+
return "locked";
|
|
1288
|
+
}
|
|
1289
|
+
const submitted = Buffer.from(hashCode(code.trim()));
|
|
1290
|
+
const expected = Buffer.from(storedHash);
|
|
1291
|
+
const matches = submitted.length === expected.length && crypto.timingSafeEqual(submitted, expected);
|
|
1292
|
+
if (!matches) return "invalid";
|
|
1293
|
+
const deleted = await redis3.del(key);
|
|
1294
|
+
await redis3.del(counter);
|
|
1295
|
+
return deleted > 0 ? "valid" : "expired";
|
|
1296
|
+
};
|
|
1297
|
+
var clearEmailCode = async (purpose, identity) => {
|
|
1298
|
+
await redis3.del(codeKey(purpose, identity));
|
|
1299
|
+
await redis3.del(attemptsKey(purpose, identity));
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// src/totp.ts
|
|
1303
|
+
import crypto2 from "crypto";
|
|
1304
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1305
|
+
var base32Encode = (input) => {
|
|
1306
|
+
let bits = 0;
|
|
1307
|
+
let value = 0;
|
|
1308
|
+
let output = "";
|
|
1309
|
+
for (const byte of input) {
|
|
1310
|
+
value = value << 8 | byte;
|
|
1311
|
+
bits += 8;
|
|
1312
|
+
while (bits >= 5) {
|
|
1313
|
+
output += BASE32_ALPHABET[value >>> bits - 5 & 31];
|
|
1314
|
+
bits -= 5;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (bits > 0) output += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
1318
|
+
return output;
|
|
1319
|
+
};
|
|
1320
|
+
var base32Decode = (input) => {
|
|
1321
|
+
const normalized = input.toUpperCase().replaceAll(/[\s=]/g, "");
|
|
1322
|
+
if (normalized.length === 0) return null;
|
|
1323
|
+
let bits = 0;
|
|
1324
|
+
let value = 0;
|
|
1325
|
+
const bytes = [];
|
|
1326
|
+
for (const char of normalized) {
|
|
1327
|
+
const index = BASE32_ALPHABET.indexOf(char);
|
|
1328
|
+
if (index === -1) return null;
|
|
1329
|
+
value = value << 5 | index;
|
|
1330
|
+
bits += 5;
|
|
1331
|
+
if (bits >= 8) {
|
|
1332
|
+
bytes.push(value >>> bits - 8 & 255);
|
|
1333
|
+
bits -= 8;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return Buffer.from(bytes);
|
|
1337
|
+
};
|
|
1338
|
+
var hotp = (key, counter, digits = 6) => {
|
|
1339
|
+
const counterBuffer = Buffer.alloc(8);
|
|
1340
|
+
counterBuffer.writeBigUInt64BE(BigInt(counter));
|
|
1341
|
+
const digest = crypto2.createHmac("sha1", key).update(counterBuffer).digest();
|
|
1342
|
+
const offset = digest.readUInt8(digest.length - 1) & 15;
|
|
1343
|
+
const binary = digest.readUInt32BE(offset) & 2147483647;
|
|
1344
|
+
return String(binary % 10 ** digits).padStart(digits, "0");
|
|
1345
|
+
};
|
|
1346
|
+
var TOTP_STEP_SECONDS = 30;
|
|
1347
|
+
var verifyTotp = ({ secret, code, window = 1, timestampMs }) => {
|
|
1348
|
+
const key = base32Decode(secret);
|
|
1349
|
+
const normalizedCode = code.trim();
|
|
1350
|
+
if (!key || key.length === 0 || !/^\d{6,8}$/.test(normalizedCode)) return { valid: false, timestep: null };
|
|
1351
|
+
const now = timestampMs ?? Date.now();
|
|
1352
|
+
const currentStep = Math.floor(now / 1e3 / TOTP_STEP_SECONDS);
|
|
1353
|
+
const drift = Math.max(0, Math.min(window, 4));
|
|
1354
|
+
let matched = null;
|
|
1355
|
+
for (let step = currentStep - drift; step <= currentStep + drift; step++) {
|
|
1356
|
+
const expected = hotp(key, step, normalizedCode.length);
|
|
1357
|
+
const expectedBuffer = Buffer.from(expected);
|
|
1358
|
+
const actualBuffer = Buffer.from(normalizedCode);
|
|
1359
|
+
if (expectedBuffer.length === actualBuffer.length && crypto2.timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
1360
|
+
matched ??= step;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return matched === null ? { valid: false, timestep: null } : { valid: true, timestep: matched };
|
|
1364
|
+
};
|
|
1365
|
+
var generateTotpSecret = () => base32Encode(crypto2.randomBytes(20));
|
|
1366
|
+
var buildOtpauthUri = ({ secret, accountName, issuer }) => {
|
|
1367
|
+
const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}`;
|
|
1368
|
+
const query = new URLSearchParams({
|
|
1369
|
+
secret,
|
|
1370
|
+
issuer,
|
|
1371
|
+
algorithm: "SHA1",
|
|
1372
|
+
digits: "6",
|
|
1373
|
+
period: String(TOTP_STEP_SECONDS)
|
|
1374
|
+
});
|
|
1375
|
+
return `otpauth://totp/${label}?${query.toString()}`;
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// src/twoFactor.ts
|
|
1379
|
+
var sha256 = (value) => crypto3.createHash("sha256").update(value).digest("hex");
|
|
1380
|
+
var ENCRYPTED_PREFIX = "gcm:";
|
|
1381
|
+
var warnedPlaintextSecret = false;
|
|
1382
|
+
var encryptionKey = () => {
|
|
1383
|
+
const raw = process.env.TOTP_ENCRYPTION_KEY;
|
|
1384
|
+
if (!raw) return null;
|
|
1385
|
+
return crypto3.createHash("sha256").update(raw).digest();
|
|
1386
|
+
};
|
|
1387
|
+
var encryptTotpSecret = (secret) => {
|
|
1388
|
+
const key = encryptionKey();
|
|
1389
|
+
if (!key) {
|
|
1390
|
+
if (!warnedPlaintextSecret) {
|
|
1391
|
+
warnedPlaintextSecret = true;
|
|
1392
|
+
getLogger5().warn("[2fa] TOTP_ENCRYPTION_KEY is not set \u2014 TOTP secrets are stored unencrypted. Set it in .env.local to encrypt new enrollments at rest.");
|
|
1393
|
+
}
|
|
1394
|
+
return secret;
|
|
1395
|
+
}
|
|
1396
|
+
const iv = crypto3.randomBytes(12);
|
|
1397
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", key, iv);
|
|
1398
|
+
const ciphertext = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
|
|
1399
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${cipher.getAuthTag().toString("base64")}:${ciphertext.toString("base64")}`;
|
|
1400
|
+
};
|
|
1401
|
+
var decryptTotpSecret = (stored) => {
|
|
1402
|
+
if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored;
|
|
1403
|
+
const key = encryptionKey();
|
|
1404
|
+
if (!key) {
|
|
1405
|
+
getLogger5().error("[2fa] found an encrypted TOTP secret but TOTP_ENCRYPTION_KEY is not set \u2014 cannot verify TOTP codes.");
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
const [ivPart, tagPart, dataPart] = stored.slice(ENCRYPTED_PREFIX.length).split(":");
|
|
1409
|
+
if (!ivPart || !tagPart || !dataPart) return null;
|
|
1410
|
+
try {
|
|
1411
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", key, Buffer.from(ivPart, "base64"));
|
|
1412
|
+
decipher.setAuthTag(Buffer.from(tagPart, "base64"));
|
|
1413
|
+
return Buffer.concat([decipher.update(Buffer.from(dataPart, "base64")), decipher.final()]).toString("utf8");
|
|
1414
|
+
} catch {
|
|
1415
|
+
getLogger5().error("[2fa] TOTP secret decryption failed (wrong TOTP_ENCRYPTION_KEY?).");
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
var challengeKey = (token) => formatKey5("-2fa-challenge", sha256(token));
|
|
1420
|
+
var challengeAttemptsKey = (token) => formatKey5("-2fa-challenge-attempts", sha256(token));
|
|
1421
|
+
var usedTimestepKey = (userId, timestep) => formatKey5("-2fa-usedstep", `${userId}:${String(timestep)}`);
|
|
1422
|
+
var accountFailureKey = (userId) => formatKey5("-2fa-accountfail", userId);
|
|
1423
|
+
var RECOVERY_LEASE_MS = 5e3;
|
|
1424
|
+
var recoveryLeaseName = (userId) => `2fa-recovery:${userId}`;
|
|
1425
|
+
var pendingSecretKey = (userId) => formatKey5("-2fa-pending", userId);
|
|
1426
|
+
var ACCOUNT_FAIL_LIMIT = 10;
|
|
1427
|
+
var ACCOUNT_FAIL_WINDOW_SECONDS = 15 * 60;
|
|
1428
|
+
var isTwoFactorAccountLocked = async (userId) => {
|
|
1429
|
+
const count = Number(await redis4.get(accountFailureKey(userId)) ?? "0");
|
|
1430
|
+
return count >= ACCOUNT_FAIL_LIMIT;
|
|
1431
|
+
};
|
|
1432
|
+
var recordTwoFactorAccountFailure = async (userId) => {
|
|
1433
|
+
const key = accountFailureKey(userId);
|
|
1434
|
+
const count = await redis4.incr(key);
|
|
1435
|
+
if (count === 1) await redis4.expire(key, ACCOUNT_FAIL_WINDOW_SECONDS);
|
|
1436
|
+
};
|
|
1437
|
+
var clearTwoFactorAccountFailures = async (userId) => {
|
|
1438
|
+
await redis4.del(accountFailureKey(userId));
|
|
1439
|
+
};
|
|
1440
|
+
var twoFactorConfig = () => getProjectConfig9().auth;
|
|
1441
|
+
var availableTwoFactorMethods = (user) => {
|
|
1442
|
+
const methods = [];
|
|
1443
|
+
if (user.totpSecret) methods.push("totp");
|
|
1444
|
+
if (twoFactorConfig().twoFactorEmailFallback && user.email) methods.push("email-code");
|
|
1445
|
+
if ((user.recoveryCodes ?? []).length > 0) methods.push("recovery-code");
|
|
1446
|
+
return methods;
|
|
1447
|
+
};
|
|
1448
|
+
var createTwoFactorChallengeIfRequired = async (user, _context) => {
|
|
1449
|
+
if (twoFactorConfig().twoFactor === "disabled") return null;
|
|
1450
|
+
if (!user.twoFactorEnabled || !user.totpSecret) return null;
|
|
1451
|
+
const methods = availableTwoFactorMethods(user);
|
|
1452
|
+
const token = crypto3.randomBytes(32).toString("hex");
|
|
1453
|
+
const record = { userId: user.id, email: user.email ?? void 0 };
|
|
1454
|
+
await redis4.set(challengeKey(token), JSON.stringify(record), "EX", twoFactorConfig().twoFactorChallengeTtlSeconds);
|
|
1455
|
+
return {
|
|
1456
|
+
status: true,
|
|
1457
|
+
reason: "login.twoFactorRequired",
|
|
1458
|
+
requiresTwoFactor: true,
|
|
1459
|
+
challengeToken: token,
|
|
1460
|
+
twoFactorMethods: methods
|
|
1461
|
+
};
|
|
1462
|
+
};
|
|
1463
|
+
var readChallenge = async (token) => {
|
|
1464
|
+
if (!/^[a-f0-9]{64}$/.test(token)) return null;
|
|
1465
|
+
const raw = await redis4.get(challengeKey(token));
|
|
1466
|
+
if (!raw) return null;
|
|
1467
|
+
const [parseError, parsed] = await tryCatch3(() => Promise.resolve(JSON.parse(raw)));
|
|
1468
|
+
if (parseError || typeof parsed?.userId !== "string") return null;
|
|
1469
|
+
return parsed;
|
|
1470
|
+
};
|
|
1471
|
+
var burnChallenge = async (token) => {
|
|
1472
|
+
await redis4.del(challengeKey(token));
|
|
1473
|
+
await redis4.del(challengeAttemptsKey(token));
|
|
1474
|
+
};
|
|
1475
|
+
var verifyTotpForUser = async (user, code, enforceSingleUse) => {
|
|
1476
|
+
if (!user.totpSecret) return false;
|
|
1477
|
+
if (!/^\d{6}$/.test(code.trim())) return false;
|
|
1478
|
+
const secret = decryptTotpSecret(user.totpSecret);
|
|
1479
|
+
if (!secret) return false;
|
|
1480
|
+
const result = verifyTotp({ secret, code });
|
|
1481
|
+
if (!result.valid || result.timestep === null) return false;
|
|
1482
|
+
if (!enforceSingleUse) return true;
|
|
1483
|
+
const claimed = await redis4.set(usedTimestepKey(user.id, result.timestep), "1", "EX", 60 * 2, "NX");
|
|
1484
|
+
return claimed !== null;
|
|
1485
|
+
};
|
|
1486
|
+
var matchRecoveryHash = (hashes, code) => {
|
|
1487
|
+
const submitted = Buffer.from(sha256(code.trim().toLowerCase()));
|
|
1488
|
+
return hashes.findIndex((hash2) => {
|
|
1489
|
+
const stored = Buffer.from(hash2);
|
|
1490
|
+
return stored.length === submitted.length && crypto3.timingSafeEqual(stored, submitted);
|
|
1491
|
+
});
|
|
1492
|
+
};
|
|
1493
|
+
var consumeRecoveryCode = async (user, code) => {
|
|
1494
|
+
if ((user.recoveryCodes ?? []).length === 0) return false;
|
|
1495
|
+
const leaseToken = await acquireLease(recoveryLeaseName(user.id), RECOVERY_LEASE_MS);
|
|
1496
|
+
if (!leaseToken) {
|
|
1497
|
+
getLogger5().warn("[2fa] recovery-code burn is contended (lease busy) \u2014 refusing this attempt, retry");
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
const fresh = await getUserAdapter().findById(user.id);
|
|
1502
|
+
const hashes = fresh?.recoveryCodes ?? [];
|
|
1503
|
+
const index = matchRecoveryHash(hashes, code);
|
|
1504
|
+
if (index === -1) return false;
|
|
1505
|
+
const remaining = hashes.filter((_, position) => position !== index);
|
|
1506
|
+
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, { recoveryCodes: remaining }));
|
|
1507
|
+
if (updateError) {
|
|
1508
|
+
getLogger5().error("[2fa] could not persist recovery-code consumption \u2014 refusing the code", updateError);
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
return true;
|
|
1512
|
+
} finally {
|
|
1513
|
+
await releaseLease(recoveryLeaseName(user.id), leaseToken);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
var verifyTwoFactorChallenge = async (input) => {
|
|
1517
|
+
const challenge = await readChallenge(input.challengeToken);
|
|
1518
|
+
if (!challenge) return { status: false, reason: "login.twoFactorChallengeExpired" };
|
|
1519
|
+
if (await isTwoFactorAccountLocked(challenge.userId)) {
|
|
1520
|
+
return { status: false, reason: "login.twoFactorLocked" };
|
|
1521
|
+
}
|
|
1522
|
+
const counter = challengeAttemptsKey(input.challengeToken);
|
|
1523
|
+
const attempts = await redis4.incr(counter);
|
|
1524
|
+
const remaining = await redis4.ttl(challengeKey(input.challengeToken));
|
|
1525
|
+
if (remaining > 0) await redis4.expire(counter, remaining);
|
|
1526
|
+
if (attempts > twoFactorConfig().twoFactorMaxAttempts) {
|
|
1527
|
+
await burnChallenge(input.challengeToken);
|
|
1528
|
+
return { status: false, reason: "login.twoFactorLocked" };
|
|
1529
|
+
}
|
|
1530
|
+
const user = await getUserAdapter().findById(challenge.userId);
|
|
1531
|
+
if (!user) {
|
|
1532
|
+
await burnChallenge(input.challengeToken);
|
|
1533
|
+
return { status: false, reason: "login.twoFactorChallengeExpired" };
|
|
1534
|
+
}
|
|
1535
|
+
const method = input.method ?? "totp";
|
|
1536
|
+
let verified = false;
|
|
1537
|
+
if (method === "totp") {
|
|
1538
|
+
verified = await verifyTotpForUser(user, input.code, true);
|
|
1539
|
+
} else if (method === "email-code") {
|
|
1540
|
+
if (!twoFactorConfig().twoFactorEmailFallback) return { status: false, reason: "login.twoFactorMethodUnavailable" };
|
|
1541
|
+
verified = await verifyEmailCode({
|
|
1542
|
+
purpose: "2fa",
|
|
1543
|
+
identity: user.id,
|
|
1544
|
+
code: input.code,
|
|
1545
|
+
maxAttempts: twoFactorConfig().emailCodeMaxAttempts
|
|
1546
|
+
}) === "valid";
|
|
1547
|
+
} else {
|
|
1548
|
+
verified = await consumeRecoveryCode(user, input.code);
|
|
1549
|
+
}
|
|
1550
|
+
if (!verified) {
|
|
1551
|
+
await recordTwoFactorAccountFailure(user.id);
|
|
1552
|
+
void dispatchHook3("loginFailed", { email: user.email ?? void 0, userId: user.id, provider: "credentials", reason: "login.twoFactorInvalidCode", stage: "login", requesterIp: input.requesterIp });
|
|
1553
|
+
return { status: false, reason: "login.twoFactorInvalidCode" };
|
|
1554
|
+
}
|
|
1555
|
+
await burnChallenge(input.challengeToken);
|
|
1556
|
+
await clearEmailCode("2fa", user.id);
|
|
1557
|
+
await clearTwoFactorAccountFailures(user.id);
|
|
1558
|
+
return finalizeLogin(user, {
|
|
1559
|
+
provider: "credentials",
|
|
1560
|
+
email: user.email ?? void 0,
|
|
1561
|
+
supersedeToken: input.supersedeToken,
|
|
1562
|
+
requesterIp: input.requesterIp
|
|
1563
|
+
});
|
|
1564
|
+
};
|
|
1565
|
+
var requestTwoFactorEmailCode = async (challengeToken) => {
|
|
1566
|
+
if (!twoFactorConfig().twoFactorEmailFallback) return { ok: false, reason: "login.twoFactorMethodUnavailable" };
|
|
1567
|
+
const challenge = await readChallenge(challengeToken);
|
|
1568
|
+
if (!challenge) return { ok: false, reason: "login.twoFactorChallengeExpired" };
|
|
1569
|
+
const user = await getUserAdapter().findById(challenge.userId);
|
|
1570
|
+
if (!user?.email) return { ok: false, reason: "login.twoFactorChallengeExpired" };
|
|
1571
|
+
const config = twoFactorConfig();
|
|
1572
|
+
const code = await issueEmailCode({
|
|
1573
|
+
purpose: "2fa",
|
|
1574
|
+
identity: user.id,
|
|
1575
|
+
ttlSeconds: config.emailCodeTtlSeconds,
|
|
1576
|
+
digits: config.emailCodeLength
|
|
1577
|
+
});
|
|
1578
|
+
const sent = await sendCodeEmail(user.email, code, "Your verification code", `Use this code to finish signing in to ${getProjectName()}.`);
|
|
1579
|
+
return sent ? { ok: true } : { ok: false, reason: "login.emailCodeSendFailed" };
|
|
1580
|
+
};
|
|
1581
|
+
var sendCodeEmail = async (to, code, subject, intro) => {
|
|
1582
|
+
const [loadError, email] = await tryCatch3(() => loadEmailModule());
|
|
1583
|
+
if (loadError || !email) {
|
|
1584
|
+
getLogger5().error("[2fa/email-code] @luckystack/email is not installed \u2014 cannot send the code email.");
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
const spaced = code.match(/\d/g)?.join(" ") ?? code;
|
|
1588
|
+
const [sendError, result] = await tryCatch3(() => email.sendEmail({
|
|
1589
|
+
to,
|
|
1590
|
+
subject,
|
|
1591
|
+
html: `<p>${intro}</p><p style="font-size:28px;font-weight:700;letter-spacing:6px;font-family:monospace">${spaced}</p><p>The code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.</p>`,
|
|
1592
|
+
text: `${intro}
|
|
1593
|
+
|
|
1594
|
+
Code: ${code}
|
|
1595
|
+
|
|
1596
|
+
The code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.`,
|
|
1597
|
+
adapterHint: "transactional"
|
|
1598
|
+
}));
|
|
1599
|
+
if (sendError || !result?.ok) {
|
|
1600
|
+
getLogger5().error("[2fa/email-code] sending the code email failed", sendError ?? new Error(result?.reason ?? "unknown"));
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
return true;
|
|
1604
|
+
};
|
|
1605
|
+
var enrollmentBlockedReason = (user) => {
|
|
1606
|
+
if (twoFactorConfig().twoFactor === "disabled") return "login.twoFactorDisabledByServer";
|
|
1607
|
+
if (user.twoFactorEnabled) return "login.twoFactorAlreadyEnabled";
|
|
1608
|
+
return null;
|
|
1609
|
+
};
|
|
1610
|
+
var beginTotpEnrollment = async (user) => {
|
|
1611
|
+
const blocked = enrollmentBlockedReason(user);
|
|
1612
|
+
if (blocked) return { ok: false, reason: blocked };
|
|
1613
|
+
const secret = generateTotpSecret();
|
|
1614
|
+
await redis4.set(pendingSecretKey(user.id), encryptTotpSecret(secret), "EX", 60 * 10);
|
|
1615
|
+
return {
|
|
1616
|
+
ok: true,
|
|
1617
|
+
secret,
|
|
1618
|
+
otpauthUri: buildOtpauthUri({ secret, accountName: user.email ?? user.id, issuer: getProjectName() })
|
|
1619
|
+
};
|
|
1620
|
+
};
|
|
1621
|
+
var generateRecoveryCodes = () => {
|
|
1622
|
+
const raw = Array.from({ length: 10 }, () => {
|
|
1623
|
+
const hex = crypto3.randomBytes(10).toString("hex");
|
|
1624
|
+
return `${hex.slice(0, 5)}-${hex.slice(5, 10)}-${hex.slice(10, 15)}-${hex.slice(15, 20)}`;
|
|
1625
|
+
});
|
|
1626
|
+
return { raw, hashes: raw.map((code) => sha256(code.toLowerCase())) };
|
|
1627
|
+
};
|
|
1628
|
+
var confirmTotpEnrollment = async (user, code) => {
|
|
1629
|
+
const blocked = enrollmentBlockedReason(user);
|
|
1630
|
+
if (blocked) return { ok: false, reason: blocked };
|
|
1631
|
+
const pendingStored = await redis4.get(pendingSecretKey(user.id));
|
|
1632
|
+
if (!pendingStored) return { ok: false, reason: "login.twoFactorEnrollmentExpired" };
|
|
1633
|
+
const secret = decryptTotpSecret(pendingStored);
|
|
1634
|
+
if (!secret) return { ok: false, reason: "login.twoFactorEnrollmentExpired" };
|
|
1635
|
+
const result = verifyTotp({ secret, code });
|
|
1636
|
+
if (!result.valid) return { ok: false, reason: "login.twoFactorInvalidCode" };
|
|
1637
|
+
const { raw, hashes } = generateRecoveryCodes();
|
|
1638
|
+
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, {
|
|
1639
|
+
totpSecret: encryptTotpSecret(secret),
|
|
1640
|
+
twoFactorEnabled: true,
|
|
1641
|
+
recoveryCodes: hashes
|
|
1642
|
+
}));
|
|
1643
|
+
if (updateError) {
|
|
1644
|
+
getLogger5().error("[2fa] enrollment persist failed \u2014 is your UserAdapter/schema missing the 2FA fields? (twoFactorEnabled, totpSecret, recoveryCodes)", updateError);
|
|
1645
|
+
return { ok: false, reason: "login.twoFactorPersistFailed" };
|
|
1646
|
+
}
|
|
1647
|
+
await redis4.del(pendingSecretKey(user.id));
|
|
1648
|
+
return { ok: true, recoveryCodes: raw };
|
|
1649
|
+
};
|
|
1650
|
+
var disableTwoFactor = async (user, code) => {
|
|
1651
|
+
const viaTotp = await verifyTotpForUser(user, code, false);
|
|
1652
|
+
const viaRecovery = viaTotp ? false : await consumeRecoveryCode(user, code);
|
|
1653
|
+
if (!viaTotp && !viaRecovery) return { ok: false, reason: "login.twoFactorInvalidCode" };
|
|
1654
|
+
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, {
|
|
1655
|
+
totpSecret: null,
|
|
1656
|
+
twoFactorEnabled: false,
|
|
1657
|
+
recoveryCodes: []
|
|
1658
|
+
}));
|
|
1659
|
+
if (updateError) {
|
|
1660
|
+
getLogger5().error("[2fa] disable persist failed", updateError);
|
|
1661
|
+
return { ok: false, reason: "login.twoFactorPersistFailed" };
|
|
1662
|
+
}
|
|
1663
|
+
await clearTwoFactorAccountFailures(user.id);
|
|
1664
|
+
return { ok: true };
|
|
1665
|
+
};
|
|
1666
|
+
var regenerateRecoveryCodes = async (user, code) => {
|
|
1667
|
+
if (!await verifyTotpForUser(user, code, false)) return { ok: false, reason: "login.twoFactorInvalidCode" };
|
|
1668
|
+
const { raw, hashes } = generateRecoveryCodes();
|
|
1669
|
+
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, { recoveryCodes: hashes }));
|
|
1670
|
+
if (updateError) {
|
|
1671
|
+
getLogger5().error("[2fa] recovery-code regenerate persist failed", updateError);
|
|
1672
|
+
return { ok: false, reason: "login.twoFactorPersistFailed" };
|
|
1673
|
+
}
|
|
1674
|
+
return { ok: true, recoveryCodes: raw };
|
|
1675
|
+
};
|
|
1676
|
+
registerTwoFactorGate(createTwoFactorChallengeIfRequired);
|
|
1677
|
+
|
|
1678
|
+
// src/emailCodeLogin.ts
|
|
1679
|
+
import { checkRateLimit as checkRateLimit2, getLogger as getLogger6, getProjectConfig as getProjectConfig10, getProjectName as getProjectName2, tryCatch as tryCatch4 } from "@luckystack/core";
|
|
1680
|
+
var emailCodeConfig = () => getProjectConfig10().auth;
|
|
1681
|
+
var normalizeEmail = (email) => email.trim().toLowerCase();
|
|
1682
|
+
var REQUEST_WINDOW_MS = 15 * 60 * 1e3;
|
|
1683
|
+
var REQUESTS_PER_EMAIL = 3;
|
|
1684
|
+
var REQUESTS_PER_IP = 10;
|
|
1685
|
+
var requestEmailLoginCode = async ({ email, requesterIp }) => {
|
|
1686
|
+
const config = emailCodeConfig();
|
|
1687
|
+
if (!config.emailCodeLogin) return { ok: false, reason: "login.emailCodeDisabled" };
|
|
1688
|
+
const normalized = normalizeEmail(email);
|
|
1689
|
+
if (!normalized || normalized.length > config.emailMaxLength) return { ok: false, reason: "login.invalidEmailFormat" };
|
|
1690
|
+
const perEmail = await checkRateLimit2({ key: `email-code:req:${normalized}`, limit: REQUESTS_PER_EMAIL, windowMs: REQUEST_WINDOW_MS });
|
|
1691
|
+
const perIp = requesterIp ? await checkRateLimit2({ key: `email-code:req-ip:${requesterIp}`, limit: REQUESTS_PER_IP, windowMs: REQUEST_WINDOW_MS }) : { allowed: true };
|
|
1692
|
+
if (!perEmail.allowed || !perIp.allowed) return { ok: false, reason: "api.rateLimitExceeded" };
|
|
1693
|
+
const [findError, user] = await tryCatch4(
|
|
1694
|
+
() => resolveUserByEmail(getUserAdapter(), { email: normalized, provider: "credentials" })
|
|
1695
|
+
);
|
|
1696
|
+
if (findError) {
|
|
1697
|
+
getLogger6().error("email-code login: user lookup failed", findError);
|
|
1698
|
+
return { ok: true };
|
|
1699
|
+
}
|
|
1700
|
+
if (!user) {
|
|
1701
|
+
getLogger6().info("[email-code] request for unknown address (not sent)");
|
|
1702
|
+
return { ok: true };
|
|
1703
|
+
}
|
|
1704
|
+
const code = await issueEmailCode({
|
|
1705
|
+
purpose: "login",
|
|
1706
|
+
identity: normalized,
|
|
1707
|
+
ttlSeconds: config.emailCodeTtlSeconds,
|
|
1708
|
+
digits: config.emailCodeLength
|
|
1709
|
+
});
|
|
1710
|
+
void sendCodeEmail(
|
|
1711
|
+
normalized,
|
|
1712
|
+
code,
|
|
1713
|
+
"Your sign-in code",
|
|
1714
|
+
`Use this code to sign in to ${getProjectName2()}.`
|
|
1715
|
+
).then((sent) => {
|
|
1716
|
+
if (!sent) getLogger6().error("[email-code] login code send failed (account exists) \u2014 check the email adapter");
|
|
1717
|
+
});
|
|
1718
|
+
return { ok: true };
|
|
1719
|
+
};
|
|
1720
|
+
var verifyEmailLoginCode = async (input) => {
|
|
1721
|
+
const config = emailCodeConfig();
|
|
1722
|
+
if (!config.emailCodeLogin) return { status: false, reason: "login.emailCodeDisabled" };
|
|
1723
|
+
const normalized = normalizeEmail(input.email);
|
|
1724
|
+
const verdict = await verifyEmailCode({
|
|
1725
|
+
purpose: "login",
|
|
1726
|
+
identity: normalized,
|
|
1727
|
+
code: input.code,
|
|
1728
|
+
maxAttempts: config.emailCodeMaxAttempts
|
|
1729
|
+
});
|
|
1730
|
+
if (verdict !== "valid") {
|
|
1731
|
+
const reason = verdict === "invalid" ? "login.emailCodeInvalid" : verdict === "locked" ? "login.emailCodeLocked" : "login.emailCodeExpired";
|
|
1732
|
+
return { status: false, reason };
|
|
1733
|
+
}
|
|
1734
|
+
const [findError, user] = await tryCatch4(
|
|
1735
|
+
() => resolveUserByEmail(getUserAdapter(), { email: normalized, provider: "credentials" })
|
|
1736
|
+
);
|
|
1737
|
+
if (findError || !user) {
|
|
1738
|
+
getLogger6().error("email-code login: user vanished between issue and verify", findError ?? void 0);
|
|
1739
|
+
return { status: false, reason: "login.emailCodeExpired" };
|
|
1740
|
+
}
|
|
1741
|
+
await clearEmailCode("login", normalized);
|
|
1742
|
+
const challenge = await createTwoFactorChallengeIfRequired(user, { requesterIp: input.requesterIp });
|
|
1743
|
+
if (challenge) return challenge;
|
|
1744
|
+
return finalizeLogin(user, {
|
|
1745
|
+
provider: "credentials",
|
|
1746
|
+
email: normalized,
|
|
1747
|
+
supersedeToken: input.supersedeToken,
|
|
1748
|
+
requesterIp: input.requesterIp
|
|
1749
|
+
});
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1241
1752
|
// src/index.ts
|
|
1242
1753
|
registerSessionProvider({
|
|
1243
1754
|
getSession,
|
|
@@ -1251,17 +1762,29 @@ export {
|
|
|
1251
1762
|
PasswordPolicyError,
|
|
1252
1763
|
activeUsersKeyFor,
|
|
1253
1764
|
asOAuthUserData,
|
|
1765
|
+
availableTwoFactorMethods,
|
|
1766
|
+
base32Decode,
|
|
1767
|
+
base32Encode,
|
|
1768
|
+
beginTotpEnrollment,
|
|
1769
|
+
buildOtpauthUri,
|
|
1254
1770
|
clearAuthFailures,
|
|
1771
|
+
clearEmailCode,
|
|
1772
|
+
confirmTotpEnrollment,
|
|
1255
1773
|
consumeEmailChangeToken,
|
|
1256
1774
|
consumePasswordResetToken,
|
|
1257
1775
|
createEmailChangeToken,
|
|
1258
1776
|
createOAuthState,
|
|
1259
1777
|
createPasswordResetToken,
|
|
1778
|
+
createTwoFactorChallengeIfRequired,
|
|
1260
1779
|
credentialsProvider,
|
|
1261
1780
|
defaultPrismaUserAdapter,
|
|
1262
1781
|
deleteSession,
|
|
1782
|
+
disableTwoFactor,
|
|
1263
1783
|
discordProvider,
|
|
1264
1784
|
facebookProvider,
|
|
1785
|
+
finalizeLogin,
|
|
1786
|
+
generateNumericCode,
|
|
1787
|
+
generateTotpSecret,
|
|
1265
1788
|
getAllSessions,
|
|
1266
1789
|
getOAuthProviders,
|
|
1267
1790
|
getPostLoginRedirect,
|
|
@@ -1271,9 +1794,11 @@ export {
|
|
|
1271
1794
|
getUserAdapter,
|
|
1272
1795
|
githubProvider,
|
|
1273
1796
|
googleProvider,
|
|
1797
|
+
hotp,
|
|
1274
1798
|
isAccountLocked,
|
|
1275
1799
|
isFullOAuthProvider,
|
|
1276
1800
|
isUserAdapterRegistered,
|
|
1801
|
+
issueEmailCode,
|
|
1277
1802
|
loginCallback,
|
|
1278
1803
|
loginWithCredentials,
|
|
1279
1804
|
loginWithCredentialsCore,
|
|
@@ -1281,13 +1806,17 @@ export {
|
|
|
1281
1806
|
microsoftProvider,
|
|
1282
1807
|
recordAuthFailure,
|
|
1283
1808
|
redisSessionAdapter,
|
|
1809
|
+
regenerateRecoveryCodes,
|
|
1284
1810
|
registerOAuthProviderFactory,
|
|
1285
1811
|
registerOAuthProviders,
|
|
1286
1812
|
registerPostLoginRedirect,
|
|
1287
1813
|
registerSessionAdapter,
|
|
1288
1814
|
registerSessionSanitizer,
|
|
1815
|
+
registerTwoFactorGate,
|
|
1289
1816
|
registerUserAdapter,
|
|
1290
1817
|
registerWithCredentials,
|
|
1818
|
+
requestEmailLoginCode,
|
|
1819
|
+
requestTwoFactorEmailCode,
|
|
1291
1820
|
revokeUserSessions,
|
|
1292
1821
|
saveSession,
|
|
1293
1822
|
sendEmailChangeConfirmation,
|
|
@@ -1295,6 +1824,10 @@ export {
|
|
|
1295
1824
|
sessionKeyFor,
|
|
1296
1825
|
updatePasswordHash,
|
|
1297
1826
|
validatePassword,
|
|
1298
|
-
|
|
1827
|
+
verifyEmailCode,
|
|
1828
|
+
verifyEmailLoginCode,
|
|
1829
|
+
verifyPassword,
|
|
1830
|
+
verifyTotp,
|
|
1831
|
+
verifyTwoFactorChallenge
|
|
1299
1832
|
};
|
|
1300
1833
|
//# sourceMappingURL=index.js.map
|