najm-auth 2.0.9 → 2.0.10

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.
@@ -185,7 +185,12 @@ async function requestSessionRecovery(options) {
185
185
  } catch (error) {
186
186
  reportFailure(options, {
187
187
  reason: "fetch-error",
188
- error: safeErrorDetails(error)
188
+ error: safeErrorDetails(error, [
189
+ options.refreshCookieValue,
190
+ options.sessionSecret,
191
+ endpoint,
192
+ options.requestOrigin
193
+ ])
189
194
  });
190
195
  return { status: "unavailable" };
191
196
  }
@@ -289,33 +294,37 @@ function reportFailure(options, failure) {
289
294
  }
290
295
  }
291
296
  __name(reportFailure, "reportFailure");
292
- function safeErrorDetails(value) {
297
+ function safeErrorDetails(value, sensitiveValues) {
293
298
  const error = isRecord2(value) ? value : {};
294
299
  const details = {
295
- name: safeText(error.name, "Error"),
296
- message: safeText(error.message, "Session recovery fetch failed")
300
+ name: safeText(error.name, "Error", sensitiveValues),
301
+ message: safeText(error.message, "Session recovery fetch failed", sensitiveValues)
297
302
  };
298
- const code = safeOptionalText(error.code);
303
+ const code = safeOptionalText(error.code, sensitiveValues);
299
304
  if (code) details.code = code;
300
305
  if (isRecord2(error.cause)) {
301
- const causeCode = safeOptionalText(error.cause.code);
306
+ const causeCode = safeOptionalText(error.cause.code, sensitiveValues);
302
307
  details.cause = {
303
- name: safeText(error.cause.name, "Error"),
304
- message: safeText(error.cause.message, "Session recovery fetch failed"),
308
+ name: safeText(error.cause.name, "Error", sensitiveValues),
309
+ message: safeText(error.cause.message, "Session recovery fetch failed", sensitiveValues),
305
310
  ...causeCode ? { code: causeCode } : {}
306
311
  };
307
312
  }
308
313
  return details;
309
314
  }
310
315
  __name(safeErrorDetails, "safeErrorDetails");
311
- function safeText(value, fallback) {
316
+ function safeText(value, fallback, sensitiveValues) {
312
317
  if (typeof value !== "string" || !value) return fallback;
313
- return value.replace(/[\u0000-\u001F\u007F]/g, " ").slice(0, 300);
318
+ let safe = value.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\b(authorization|cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
319
+ for (const sensitive of sensitiveValues) {
320
+ if (sensitive) safe = safe.split(sensitive).join("[redacted]");
321
+ }
322
+ return safe.slice(0, 300);
314
323
  }
315
324
  __name(safeText, "safeText");
316
- function safeOptionalText(value) {
325
+ function safeOptionalText(value, sensitiveValues) {
317
326
  if (typeof value !== "string" && typeof value !== "number") return void 0;
318
- return safeText(String(value), "");
327
+ return safeText(String(value), "", sensitiveValues);
319
328
  }
320
329
  __name(safeOptionalText, "safeOptionalText");
321
330
  function isRecord2(value) {
@@ -197,7 +197,12 @@ async function requestSessionRecovery(options) {
197
197
  } catch (error) {
198
198
  reportFailure(options, {
199
199
  reason: "fetch-error",
200
- error: safeErrorDetails(error)
200
+ error: safeErrorDetails(error, [
201
+ options.refreshCookieValue,
202
+ options.sessionSecret,
203
+ endpoint,
204
+ options.requestOrigin
205
+ ])
201
206
  });
202
207
  return { status: "unavailable" };
203
208
  }
@@ -292,31 +297,35 @@ function reportFailure(options, failure) {
292
297
  } catch {
293
298
  }
294
299
  }
295
- function safeErrorDetails(value) {
300
+ function safeErrorDetails(value, sensitiveValues) {
296
301
  const error = isRecord2(value) ? value : {};
297
302
  const details = {
298
- name: safeText(error.name, "Error"),
299
- message: safeText(error.message, "Session recovery fetch failed")
303
+ name: safeText(error.name, "Error", sensitiveValues),
304
+ message: safeText(error.message, "Session recovery fetch failed", sensitiveValues)
300
305
  };
301
- const code = safeOptionalText(error.code);
306
+ const code = safeOptionalText(error.code, sensitiveValues);
302
307
  if (code) details.code = code;
303
308
  if (isRecord2(error.cause)) {
304
- const causeCode = safeOptionalText(error.cause.code);
309
+ const causeCode = safeOptionalText(error.cause.code, sensitiveValues);
305
310
  details.cause = {
306
- name: safeText(error.cause.name, "Error"),
307
- message: safeText(error.cause.message, "Session recovery fetch failed"),
311
+ name: safeText(error.cause.name, "Error", sensitiveValues),
312
+ message: safeText(error.cause.message, "Session recovery fetch failed", sensitiveValues),
308
313
  ...causeCode ? { code: causeCode } : {}
309
314
  };
310
315
  }
311
316
  return details;
312
317
  }
313
- function safeText(value, fallback) {
318
+ function safeText(value, fallback, sensitiveValues) {
314
319
  if (typeof value !== "string" || !value) return fallback;
315
- return value.replace(/[\u0000-\u001F\u007F]/g, " ").slice(0, 300);
320
+ let safe = value.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\b(authorization|cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
321
+ for (const sensitive of sensitiveValues) {
322
+ if (sensitive) safe = safe.split(sensitive).join("[redacted]");
323
+ }
324
+ return safe.slice(0, 300);
316
325
  }
317
- function safeOptionalText(value) {
326
+ function safeOptionalText(value, sensitiveValues) {
318
327
  if (typeof value !== "string" && typeof value !== "number") return void 0;
319
- return safeText(String(value), "");
328
+ return safeText(String(value), "", sensitiveValues);
320
329
  }
321
330
  function isRecord2(value) {
322
331
  return typeof value === "object" && value !== null;
package/dist/index.d.ts CHANGED
@@ -820,6 +820,7 @@ declare class UserService {
820
820
  findByEmailInsensitive(email: string): Promise<(User & {
821
821
  role?: string | null;
822
822
  }) | undefined>;
823
+ findByPhone(phone: string): Promise<UserWithPermissions>;
823
824
  getAuthRecordById(id: string): Promise<User | undefined>;
824
825
  create(data: Record<string, any>): Promise<SanitizedUser>;
825
826
  update(id: string, data: Record<string, any>): Promise<SanitizedUser>;
@@ -1144,10 +1145,13 @@ declare const inviteUserDto: z.ZodObject<{
1144
1145
  declare const userIdParam: z.ZodObject<{
1145
1146
  id: z.ZodString;
1146
1147
  }, z.core.$strip>;
1147
- declare const loginDto: z.ZodObject<{
1148
+ declare const loginDto: z.ZodUnion<readonly [z.ZodObject<{
1149
+ identifier: z.ZodString;
1150
+ password: z.ZodString;
1151
+ }, z.core.$strip>, z.ZodObject<{
1148
1152
  email: z.ZodString;
1149
1153
  password: z.ZodString;
1150
- }, z.core.$strip>;
1154
+ }, z.core.$strip>]>;
1151
1155
  declare const changePasswordDto: z.ZodObject<{
1152
1156
  currentPassword: z.ZodString;
1153
1157
  newPassword: z.ZodString;
@@ -1306,6 +1310,12 @@ declare class AuthService {
1306
1310
  }>;
1307
1311
  }
1308
1312
 
1313
+ /**
1314
+ * Composite key: IP + hashed normalized login/registration identity.
1315
+ * Buckets rate limits per IP+credential combo so different users
1316
+ * on the same IP (e.g. localhost, NAT) don't share a single bucket.
1317
+ */
1318
+ declare const authIdentityRateLimitKey: (ctx: Context) => Promise<string>;
1309
1319
  declare class AuthController {
1310
1320
  private authService;
1311
1321
  constructor(authService: AuthService);
@@ -1419,6 +1429,15 @@ declare class AuthResolver {
1419
1429
  onReady(): Promise<void>;
1420
1430
  }
1421
1431
 
1432
+ /**
1433
+ * Normalize an authentication identifier for both login resolution and
1434
+ * rate-limit bucketing. Email comparison is case-insensitive. Phone login uses
1435
+ * an E.164-compatible value and accepts common visual separators plus a `00`
1436
+ * international prefix.
1437
+ */
1438
+ declare function normalizeAuthIdentifier(value: unknown): string | null;
1439
+ declare function isEmailIdentifier(value: string): boolean;
1440
+
1422
1441
  interface RunAsUser {
1423
1442
  id: string;
1424
1443
  role?: string | null;
@@ -2317,4 +2336,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
2317
2336
  */
2318
2337
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
2319
2338
 
2320
- export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
2339
+ export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
package/dist/index.js CHANGED
@@ -1260,6 +1260,9 @@ var UserService = class UserService2 {
1260
1260
  async findByEmailInsensitive(email2) {
1261
1261
  return await this.userRepository.getByEmailInsensitive(email2);
1262
1262
  }
1263
+ async findByPhone(phone) {
1264
+ return await this.userRepository.findByPhone(phone);
1265
+ }
1263
1266
  async getAuthRecordById(id) {
1264
1267
  return await this.userRepository.getRawById(id);
1265
1268
  }
@@ -1575,7 +1578,7 @@ var TokenService = class TokenService2 {
1575
1578
  if (authorization?.startsWith("Bearer ")) {
1576
1579
  return authorization.split(" ")[1];
1577
1580
  }
1578
- Err6(this.t("errors.tokenMissing"));
1581
+ Err6(this.t("errors.tokenMissing"), 401);
1579
1582
  }
1580
1583
  /**
1581
1584
  * Verify access token and check blacklist
@@ -1586,7 +1589,7 @@ var TokenService = class TokenService2 {
1586
1589
  try {
1587
1590
  payload = jwt.verify(token, this.config.jwt.accessSecret);
1588
1591
  } catch {
1589
- Err6(this.t("errors.tokenVerificationFailed"));
1592
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1590
1593
  }
1591
1594
  const sessionKey = this.sessionVersionKey(payload.userId);
1592
1595
  const blacklistKey = payload.jti ? `${this.blacklistPrefix}${payload.jti}` : null;
@@ -1599,15 +1602,15 @@ var TokenService = class TokenService2 {
1599
1602
  const values = await this.getCacheValues(keys);
1600
1603
  const valueByKey = new Map(keys.map((key, i) => [key, values[i]]));
1601
1604
  if (blacklistKey && valueByKey.get(blacklistKey) != null) {
1602
- Err6(this.t("errors.tokenRevoked"));
1605
+ Err6(this.t("errors.tokenRevoked"), 401);
1603
1606
  }
1604
1607
  if (familyKey && valueByKey.get(familyKey) != null) {
1605
- Err6(this.t("errors.tokenRevoked"));
1608
+ Err6(this.t("errors.tokenRevoked"), 401);
1606
1609
  }
1607
1610
  const activeSessionVersion = this.parseSessionVersion(valueByKey.get(sessionKey) ?? null);
1608
1611
  const tokenSessionVersion = payload.sessionVersion ?? 0;
1609
1612
  if (tokenSessionVersion !== activeSessionVersion) {
1610
- Err6(this.t("errors.tokenRevoked"));
1613
+ Err6(this.t("errors.tokenRevoked"), 401);
1611
1614
  }
1612
1615
  return payload;
1613
1616
  }
@@ -1616,13 +1619,13 @@ var TokenService = class TokenService2 {
1616
1619
  try {
1617
1620
  decoded = jwt.verify(token, this.config.jwt.refreshSecret);
1618
1621
  } catch {
1619
- Err6(this.t("errors.tokenVerificationFailed"));
1622
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1620
1623
  }
1621
1624
  if (decoded.type && decoded.type !== "refresh") {
1622
- Err6(this.t("errors.tokenVerificationFailed"));
1625
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1623
1626
  }
1624
1627
  if (!decoded.tokenFamily) {
1625
- Err6(this.t("errors.tokenVerificationFailed"));
1628
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1626
1629
  }
1627
1630
  return { userId: decoded.userId, tokenFamily: decoded.tokenFamily };
1628
1631
  }
@@ -1641,12 +1644,12 @@ var TokenService = class TokenService2 {
1641
1644
  async resolveRefreshSessionFromCookie() {
1642
1645
  const refreshToken = this.cookieManager.getRefreshToken();
1643
1646
  if (!refreshToken) {
1644
- Err6(this.t("errors.refreshTokenMissing"));
1647
+ Err6(this.t("errors.refreshTokenMissing"), 401);
1645
1648
  }
1646
1649
  const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
1647
1650
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
1648
1651
  if (!stored || stored.userId !== userId) {
1649
- Err6(this.t("errors.refreshTokenInvalid"));
1652
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1650
1653
  }
1651
1654
  const presentedHash = this.hashToken(refreshToken);
1652
1655
  if (presentedHash === stored.token) {
@@ -1656,7 +1659,7 @@ var TokenService = class TokenService2 {
1656
1659
  if (canRecover) {
1657
1660
  return { userId, tokenFamily };
1658
1661
  }
1659
- Err6(this.t("errors.refreshTokenInvalid"));
1662
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1660
1663
  }
1661
1664
  async resolveUserFromCookie() {
1662
1665
  return (await this.resolveRefreshSessionFromCookie()).userId;
@@ -1833,12 +1836,12 @@ var TokenService = class TokenService2 {
1833
1836
  async refreshTokens() {
1834
1837
  const refreshToken = this.cookieManager.getRefreshToken();
1835
1838
  if (!refreshToken) {
1836
- Err6(this.t("errors.refreshTokenMissing"));
1839
+ Err6(this.t("errors.refreshTokenMissing"), 401);
1837
1840
  }
1838
1841
  const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
1839
1842
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
1840
1843
  if (!stored || stored.userId !== userId) {
1841
- Err6(this.t("errors.refreshTokenInvalid"));
1844
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1842
1845
  }
1843
1846
  const presentedHash = this.hashToken(refreshToken);
1844
1847
  await this.requireActiveRefreshUser(userId, tokenFamily);
@@ -1849,18 +1852,18 @@ var TokenService = class TokenService2 {
1849
1852
  if (canRecover) {
1850
1853
  const claimed = await this.tokenRepository.markPreviousUsed(tokenFamily, presentedHash);
1851
1854
  if (!claimed?.length) {
1852
- Err6(this.t("errors.refreshTokenInvalid"));
1855
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1853
1856
  }
1854
1857
  return this.generateTokens(userId, tokenFamily);
1855
1858
  }
1856
1859
  await this.revokeSuspectRefreshFamily(userId, tokenFamily);
1857
- Err6(this.t("errors.refreshTokenInvalid"));
1860
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1858
1861
  }
1859
1862
  async requireActiveRefreshUser(userId, tokenFamily) {
1860
1863
  const user = await this.tokenRepository.getUser(userId);
1861
1864
  if (!user || user.status !== "active") {
1862
1865
  await this.revokeFamily(tokenFamily);
1863
- Err6(this.t("errors.refreshTokenInvalid"));
1866
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1864
1867
  }
1865
1868
  return user;
1866
1869
  }
@@ -1894,7 +1897,7 @@ var TokenService = class TokenService2 {
1894
1897
  const userId = await this.resolveUserFromCookie();
1895
1898
  const user = await this.getUserById(userId);
1896
1899
  if (!user) {
1897
- Err6(this.t("errors.refreshTokenInvalid"));
1900
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1898
1901
  }
1899
1902
  return user;
1900
1903
  }
@@ -2126,6 +2129,27 @@ AuthSessionService = __decorate11([
2126
2129
  __metadata11("design:paramtypes", [typeof (_a7 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a7 : Object, typeof (_b5 = typeof UserService !== "undefined" && UserService) === "function" ? _b5 : Object, typeof (_c3 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c3 : Object])
2127
2130
  ], AuthSessionService);
2128
2131
 
2132
+ // src/auth/authIdentity.ts
2133
+ var PHONE_PATTERN = /^\+[1-9]\d{7,14}$/;
2134
+ function normalizeAuthIdentifier(value) {
2135
+ if (typeof value !== "string")
2136
+ return null;
2137
+ const trimmed = value.trim();
2138
+ if (!trimmed || trimmed.length > 254)
2139
+ return null;
2140
+ if (trimmed.includes("@")) {
2141
+ return trimmed.toLowerCase();
2142
+ }
2143
+ const compact = trimmed.replace(/[\s().-]+/g, "");
2144
+ const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
2145
+ return PHONE_PATTERN.test(international) ? international : null;
2146
+ }
2147
+ __name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
2148
+ function isEmailIdentifier(value) {
2149
+ return value.includes("@");
2150
+ }
2151
+ __name(isEmailIdentifier, "isEmailIdentifier");
2152
+
2129
2153
  // src/auth/AuthService.ts
2130
2154
  var __decorate12 = function(decorators, target, key, desc) {
2131
2155
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2256,8 +2280,16 @@ var AuthService = class AuthService2 {
2256
2280
  return this.inviteUser(body);
2257
2281
  }
2258
2282
  async loginUser(body) {
2259
- const { email: email2, password } = body;
2260
- const user = await this.userService.findByEmail(email2);
2283
+ const password = body.password;
2284
+ const rawIdentifier = "identifier" in body ? body.identifier : body.email;
2285
+ const identifier = normalizeAuthIdentifier(rawIdentifier);
2286
+ let user;
2287
+ if (identifier && isEmailIdentifier(identifier)) {
2288
+ user = await this.userService.findByEmailInsensitive(identifier);
2289
+ } else if (identifier) {
2290
+ const phoneUser = await this.userService.findByPhone(identifier);
2291
+ user = phoneUser ? await this.userService.findByEmail(phoneUser.email) : void 0;
2292
+ }
2261
2293
  if (user?.lockoutUntil && !this.isLockoutActive(user.lockoutUntil)) {
2262
2294
  await this.userService.resetFailedAttempts(user.id);
2263
2295
  user.failedLoginAttempts = 0;
@@ -2276,10 +2308,10 @@ var AuthService = class AuthService2 {
2276
2308
  Err8(this.t("errors.accountLocked"), 423);
2277
2309
  }
2278
2310
  }
2279
- Err8(this.t("errors.invalidCredentials"));
2311
+ Err8(this.t("errors.invalidCredentials"), 401);
2280
2312
  }
2281
2313
  if (user.status !== "active") {
2282
- Err8(this.t("errors.accountInactive"));
2314
+ Err8(this.t("errors.accountInactive"), 403);
2283
2315
  }
2284
2316
  if (this.config.requireVerifiedEmail && !user.emailVerified) {
2285
2317
  Err8(this.t("errors.emailNotVerified"), 403);
@@ -2406,11 +2438,11 @@ var AuthService = class AuthService2 {
2406
2438
  async changePassword(userId, currentPassword, newPassword) {
2407
2439
  const user = await this.userService.getAuthRecordById(userId);
2408
2440
  if (!user?.password) {
2409
- Err8(this.t("errors.invalidCredentials"));
2441
+ Err8(this.t("errors.invalidCredentials"), 401);
2410
2442
  }
2411
2443
  const isValid = await this.userValidator.comparePassword(currentPassword, user.password);
2412
2444
  if (!isValid) {
2413
- Err8(this.t("errors.invalidCredentials"));
2445
+ Err8(this.t("errors.invalidCredentials"), 401);
2414
2446
  }
2415
2447
  this.userValidator.validatePasswordStrength(newPassword);
2416
2448
  await this.userService.update(userId, { password: newPassword });
@@ -2566,10 +2598,17 @@ var inviteUserDto = z.object({
2566
2598
  var userIdParam = z.object({
2567
2599
  id: z.string().min(1, "User ID is required")
2568
2600
  });
2569
- var loginDto = z.object({
2570
- email: emailField,
2571
- password: passwordField
2572
- });
2601
+ var identifierField = z.string().trim().min(1).max(254);
2602
+ var loginDto = z.union([
2603
+ z.object({
2604
+ identifier: identifierField,
2605
+ password: passwordField
2606
+ }),
2607
+ z.object({
2608
+ email: emailField,
2609
+ password: passwordField
2610
+ })
2611
+ ]);
2573
2612
  var changePasswordDto = z.object({
2574
2613
  currentPassword: passwordField,
2575
2614
  newPassword: passwordField
@@ -2622,19 +2661,19 @@ var cookieFingerprint = /* @__PURE__ */ __name(() => (ctx) => {
2622
2661
  const fingerprint = cookie ? hashKeyPart(cookie) : "none";
2623
2662
  return `${ip}:${fingerprint}`;
2624
2663
  }, "cookieFingerprint");
2625
- var ipAndEmail = /* @__PURE__ */ __name(async (ctx) => {
2664
+ var authIdentityRateLimitKey = /* @__PURE__ */ __name(async (ctx) => {
2626
2665
  const ip = ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? ctx.req.header("x-real-ip") ?? "unknown";
2627
2666
  try {
2628
2667
  const body = await ctx.req.json();
2629
- if (body?.email && typeof body.email === "string") {
2630
- const normalizedEmail = body.email.trim().toLowerCase();
2631
- if (normalizedEmail)
2632
- return `${ip}:${hashKeyPart(normalizedEmail)}`;
2668
+ const identity = body?.identifier ?? body?.email;
2669
+ const normalizedIdentity = normalizeAuthIdentifier(identity);
2670
+ if (normalizedIdentity) {
2671
+ return `${ip}:${hashKeyPart(normalizedIdentity)}`;
2633
2672
  }
2634
2673
  } catch {
2635
2674
  }
2636
2675
  return ip;
2637
- }, "ipAndEmail");
2676
+ }, "authIdentityRateLimitKey");
2638
2677
  var AuthController = class AuthController2 {
2639
2678
  static {
2640
2679
  __name(this, "AuthController");
@@ -2681,7 +2720,7 @@ var AuthController = class AuthController2 {
2681
2720
  };
2682
2721
  __decorate15([
2683
2722
  Post("/register"),
2684
- RateLimit({ limit: 5, window: "15m", key: ipAndEmail }),
2723
+ RateLimit({ limit: 5, window: "15m", key: authIdentityRateLimitKey }),
2685
2724
  Validate(registerDto),
2686
2725
  ResMsg("auth.success.register"),
2687
2726
  __param5(0, Body()),
@@ -2691,7 +2730,7 @@ __decorate15([
2691
2730
  ], AuthController.prototype, "registerUser", null);
2692
2731
  __decorate15([
2693
2732
  Post("/login"),
2694
- RateLimit({ limit: 5, window: "15m", key: ipAndEmail, message: "Too many login attempts. Please try again later." }),
2733
+ RateLimit({ limit: 5, window: "15m", key: authIdentityRateLimitKey, message: "Too many login attempts. Please try again later." }),
2695
2734
  Validate(loginDto),
2696
2735
  ResMsg("auth.success.login"),
2697
2736
  __param5(0, Body()),
@@ -2760,7 +2799,7 @@ __decorate15([
2760
2799
  ], AuthController.prototype, "userProfile", null);
2761
2800
  __decorate15([
2762
2801
  Post("/forgot-password"),
2763
- RateLimit({ limit: 3, window: "15m", key: ipAndEmail, message: "Too many password reset requests. Please try again later." }),
2802
+ RateLimit({ limit: 3, window: "15m", key: authIdentityRateLimitKey, message: "Too many password reset requests. Please try again later." }),
2764
2803
  Validate(resetPasswordDto),
2765
2804
  ResMsg("auth.success.passwordResetSent"),
2766
2805
  __param5(0, Body()),
@@ -5699,6 +5738,7 @@ export {
5699
5738
  assignRoleDto,
5700
5739
  assignRoleParams,
5701
5740
  auth,
5741
+ authIdentityRateLimitKey,
5702
5742
  authSchema,
5703
5743
  authSeed,
5704
5744
  avatarsPath,
@@ -5723,12 +5763,14 @@ export {
5723
5763
  isAdmin,
5724
5764
  isAdministrator,
5725
5765
  isAuth,
5766
+ isEmailIdentifier,
5726
5767
  isEmpty,
5727
5768
  isFile,
5728
5769
  isPath,
5729
5770
  join2 as join,
5730
5771
  languageParam,
5731
5772
  loginDto,
5773
+ normalizeAuthIdentifier,
5732
5774
  oauthAccountsTable,
5733
5775
  own,
5734
5776
  parseSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [