@luckystack/login 0.14.0 → 0.15.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.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  registerOAuthProviderFactory,
15
15
  registerOAuthProviders,
16
16
  registerUserAdapter
17
- } from "./chunk-CLYMWF3A.js";
17
+ } from "./chunk-ML3OQ6R4.js";
18
18
  import {
19
19
  activeUsersKeyFor,
20
20
  deleteSession,
@@ -29,10 +29,10 @@ import {
29
29
  revokeUserSessions,
30
30
  saveSession,
31
31
  sessionKeyFor
32
- } from "./chunk-6IGNAMXW.js";
32
+ } from "./chunk-S7TST3X6.js";
33
33
 
34
34
  // src/index.ts
35
- import { registerSessionProvider } from "@luckystack/core";
35
+ import { registerRedactedLogKeys, registerSessionProvider } from "@luckystack/core";
36
36
 
37
37
  // src/authLockout.ts
38
38
  import {
@@ -865,7 +865,7 @@ var fetchOAuthProfile = async (provider, accessToken) => {
865
865
  const userData = asRecord(response);
866
866
  const resolvedName = readStringField(userData, provider.nameKey);
867
867
  const name = resolvedName === "" ? "didnt find a name" : resolvedName;
868
- const emailValue = userData[provider.emailKey];
868
+ const emailValue = provider.alwaysUseGetEmail ? void 0 : userData[provider.emailKey];
869
869
  let email = typeof emailValue === "string" ? emailValue : void 0;
870
870
  let emailVerified = false;
871
871
  if (email && provider.emailVerifiedKey) {
@@ -883,13 +883,14 @@ var fetchOAuthProfile = async (provider, accessToken) => {
883
883
  const avatarValue = provider.avatarKey ? readStringField(userData, provider.avatarKey) : provider.getAvatar ? await provider.getAvatar({ userData, avatarId: typeof avatarId === "string" ? avatarId : "", accessToken }) : "";
884
884
  const avatar = typeof avatarValue === "string" ? avatarValue : "";
885
885
  if (!email && provider.getEmail) {
886
- const selectedEmail = await provider.getEmail(accessToken);
886
+ const selected = await provider.getEmail(accessToken, userData);
887
+ const selectedEmail = typeof selected === "object" ? selected.email : selected;
887
888
  if (!selectedEmail) {
888
889
  getLogger3().warn("oauth: no email found via provider.getEmail", { provider: provider.name });
889
890
  return null;
890
891
  }
891
892
  email = selectedEmail;
892
- emailVerified = true;
893
+ emailVerified = typeof selected === "object" ? selected.verified : true;
893
894
  }
894
895
  if (email !== void 0) {
895
896
  const normalizedEmail = email.trim().toLowerCase();
@@ -1137,6 +1138,14 @@ var createPasswordResetToken = async (userId) => {
1137
1138
  var consumePasswordResetToken = async (token) => {
1138
1139
  return consumeOneTimeToken(PWRESET_NAMESPACE, token);
1139
1140
  };
1141
+ var invalidatePasswordResetToken = async (userId) => {
1142
+ const pointerKey = pwresetUserKey(userId);
1143
+ const priorKey = await redis.get(pointerKey);
1144
+ if (priorKey) {
1145
+ await redis.del(priorKey);
1146
+ }
1147
+ await redis.del(pointerKey);
1148
+ };
1140
1149
  var updatePasswordHash = async (userId, plaintext) => {
1141
1150
  const reason = validatePassword(plaintext);
1142
1151
  if (reason) {
@@ -1196,28 +1205,34 @@ var sendPasswordResetEmail = async ({ email, brand }) => {
1196
1205
  });
1197
1206
  return { ok: true };
1198
1207
  }
1199
- const token = await createPasswordResetToken(user.id);
1200
- void dispatchHook2("passwordResetRequested", {
1201
- email,
1202
- matched: true,
1203
- userId: user.id,
1204
- ttlSeconds: config.auth.passwordResetTtlSeconds
1205
- });
1206
- const baseUrl = (config.app.publicUrl || "").replace(/\/+$/, "");
1207
- const resetUrl = `${baseUrl}/reset-password?token=${encodeURIComponent(token)}`;
1208
- const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);
1209
- const result = await sendEmail({
1210
- to: user.email,
1211
- template: "password-reset",
1212
- data: { resetUrl, userName: user.name, brand: resolvedBrand, ttlMinutes },
1213
- adapterHint: "transactional"
1208
+ const deliverResetEmail = async () => {
1209
+ const token = await createPasswordResetToken(user.id);
1210
+ void dispatchHook2("passwordResetRequested", {
1211
+ email,
1212
+ matched: true,
1213
+ userId: user.id,
1214
+ ttlSeconds: config.auth.passwordResetTtlSeconds
1215
+ });
1216
+ const baseUrl = (config.app.publicUrl || "").replace(/\/+$/, "");
1217
+ const resetPath = config.auth.passwordResetPath || "/reset-password";
1218
+ const resetUrl = `${baseUrl}${resetPath}?token=${encodeURIComponent(token)}`;
1219
+ const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);
1220
+ const result = await sendEmail({
1221
+ to: user.email,
1222
+ template: "password-reset",
1223
+ data: { resetUrl, userName: user.name, brand: resolvedBrand, ttlMinutes },
1224
+ adapterHint: "transactional"
1225
+ });
1226
+ if (result.ok) {
1227
+ getLogger4().info("[forgotPassword] reset email dispatched", { userId: user.id });
1228
+ } else {
1229
+ getLogger4().warn("[forgotPassword] reset email send FAILED", { userId: user.id, reason: result.reason });
1230
+ }
1231
+ };
1232
+ void deliverResetEmail().catch((error) => {
1233
+ getLogger4().error("[forgotPassword] reset email delivery failed", { userId: user.id, error: String(error) });
1214
1234
  });
1215
- if (result.ok) {
1216
- getLogger4().info("[forgotPassword] reset email dispatched", { userId: user.id });
1217
- } else {
1218
- getLogger4().warn("[forgotPassword] reset email send FAILED", { userId: user.id, reason: result.reason });
1219
- }
1220
- return result.ok ? { ok: true } : { ok: false, reason: result.reason };
1235
+ return { ok: true };
1221
1236
  };
1222
1237
 
1223
1238
  // src/emailChange.ts
@@ -1247,6 +1262,14 @@ var consumeEmailChangeToken = async (token) => {
1247
1262
  }
1248
1263
  return parsed;
1249
1264
  };
1265
+ var invalidateEmailChangeToken = async (userId) => {
1266
+ const pointerKey = emailChangeUserKey(userId);
1267
+ const priorKey = await redis2.get(pointerKey);
1268
+ if (priorKey) {
1269
+ await redis2.del(priorKey);
1270
+ }
1271
+ await redis2.del(pointerKey);
1272
+ };
1250
1273
 
1251
1274
  // src/emailChangeNotification.ts
1252
1275
  import { getProjectConfig as getProjectConfig8 } from "@luckystack/core";
@@ -1770,9 +1793,13 @@ var confirmTotpEnrollment = async (user, code) => {
1770
1793
  }
1771
1794
  };
1772
1795
  var disableTwoFactor = async (user, code) => {
1796
+ if (await isTwoFactorAccountLocked(user.id)) return { ok: false, reason: "login.twoFactorLocked" };
1773
1797
  const viaTotp = await verifyTotpForUser(user, code, false);
1774
1798
  const viaRecovery = viaTotp ? false : await consumeRecoveryCode(user, code);
1775
- if (!viaTotp && !viaRecovery) return { ok: false, reason: "login.twoFactorInvalidCode" };
1799
+ if (!viaTotp && !viaRecovery) {
1800
+ await recordTwoFactorAccountFailure(user.id);
1801
+ return { ok: false, reason: "login.twoFactorInvalidCode" };
1802
+ }
1776
1803
  const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, {
1777
1804
  totpSecret: null,
1778
1805
  twoFactorEnabled: false,
@@ -1786,7 +1813,12 @@ var disableTwoFactor = async (user, code) => {
1786
1813
  return { ok: true };
1787
1814
  };
1788
1815
  var regenerateRecoveryCodes = async (user, code) => {
1789
- if (!await verifyTotpForUser(user, code, false)) return { ok: false, reason: "login.twoFactorInvalidCode" };
1816
+ if (await isTwoFactorAccountLocked(user.id)) return { ok: false, reason: "login.twoFactorLocked" };
1817
+ if (!await verifyTotpForUser(user, code, false)) {
1818
+ await recordTwoFactorAccountFailure(user.id);
1819
+ return { ok: false, reason: "login.twoFactorInvalidCode" };
1820
+ }
1821
+ await clearTwoFactorAccountFailures(user.id);
1790
1822
  const { raw, hashes } = generateRecoveryCodes();
1791
1823
  const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, { recoveryCodes: hashes }));
1792
1824
  if (updateError) {
@@ -1879,6 +1911,7 @@ registerSessionProvider({
1879
1911
  logout: ({ token, socket, userId, skipSessionDelete }) => logout({ token, socket, userId: userId ?? null, skipSessionDelete })
1880
1912
  });
1881
1913
  registerAuthLockoutHook();
1914
+ registerRedactedLogKeys(["recoverycode", "totp", "totpcode"]);
1882
1915
  export {
1883
1916
  OAUTH_STATE_COOKIE_NAME,
1884
1917
  PasswordPolicyError,
@@ -1917,6 +1950,8 @@ export {
1917
1950
  githubProvider,
1918
1951
  googleProvider,
1919
1952
  hotp,
1953
+ invalidateEmailChangeToken,
1954
+ invalidatePasswordResetToken,
1920
1955
  isAccountLocked,
1921
1956
  isFullOAuthProvider,
1922
1957
  isUserAdapterRegistered,