najm-auth 2.0.9 → 2.0.11

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
@@ -545,7 +545,7 @@ declare class UserRepository {
545
545
  private schema;
546
546
  private get users();
547
547
  private get roles();
548
- /** Shared query helper */
548
+ /** Shared query helper, scoped to the current database/transaction identity. */
549
549
  private queryHelper?;
550
550
  private get q();
551
551
  getAll(limit?: number, offset?: number): Promise<UserWithPermissions[]>;
@@ -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>;
@@ -846,7 +847,7 @@ declare class TokenRepository {
846
847
  private schema;
847
848
  private get tokens();
848
849
  private get users();
849
- /** Shared query helper */
850
+ /** Shared query helper, scoped to the current database/transaction identity. */
850
851
  private queryHelper?;
851
852
  private get q();
852
853
  /**
@@ -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
@@ -573,10 +573,14 @@ var UserRepository = class UserRepository2 {
573
573
  get roles() {
574
574
  return this.schema.roles;
575
575
  }
576
- /** Shared query helper */
576
+ /** Shared query helper, scoped to the current database/transaction identity. */
577
577
  queryHelper;
578
578
  get q() {
579
- return this.queryHelper ??= new AuthQueries(this.db, this.schema);
579
+ const db = this.db;
580
+ if (this.queryHelper?.db !== db) {
581
+ this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
582
+ }
583
+ return this.queryHelper.queries;
580
584
  }
581
585
  async getAll(limit = 50, offset = 0) {
582
586
  const allUsers = await this.db.select(this.q.userSelection()).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).limit(limit).offset(offset);
@@ -1260,6 +1264,9 @@ var UserService = class UserService2 {
1260
1264
  async findByEmailInsensitive(email2) {
1261
1265
  return await this.userRepository.getByEmailInsensitive(email2);
1262
1266
  }
1267
+ async findByPhone(phone) {
1268
+ return await this.userRepository.findByPhone(phone);
1269
+ }
1263
1270
  async getAuthRecordById(id) {
1264
1271
  return await this.userRepository.getRawById(id);
1265
1272
  }
@@ -1420,10 +1427,14 @@ var TokenRepository = class TokenRepository2 {
1420
1427
  get users() {
1421
1428
  return this.schema.users;
1422
1429
  }
1423
- /** Shared query helper */
1430
+ /** Shared query helper, scoped to the current database/transaction identity. */
1424
1431
  queryHelper;
1425
1432
  get q() {
1426
- return this.queryHelper ??= new AuthQueries(this.db, this.schema);
1433
+ const db = this.db;
1434
+ if (this.queryHelper?.db !== db) {
1435
+ this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
1436
+ }
1437
+ return this.queryHelper.queries;
1427
1438
  }
1428
1439
  /**
1429
1440
  * Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
@@ -1575,7 +1586,7 @@ var TokenService = class TokenService2 {
1575
1586
  if (authorization?.startsWith("Bearer ")) {
1576
1587
  return authorization.split(" ")[1];
1577
1588
  }
1578
- Err6(this.t("errors.tokenMissing"));
1589
+ Err6(this.t("errors.tokenMissing"), 401);
1579
1590
  }
1580
1591
  /**
1581
1592
  * Verify access token and check blacklist
@@ -1586,7 +1597,7 @@ var TokenService = class TokenService2 {
1586
1597
  try {
1587
1598
  payload = jwt.verify(token, this.config.jwt.accessSecret);
1588
1599
  } catch {
1589
- Err6(this.t("errors.tokenVerificationFailed"));
1600
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1590
1601
  }
1591
1602
  const sessionKey = this.sessionVersionKey(payload.userId);
1592
1603
  const blacklistKey = payload.jti ? `${this.blacklistPrefix}${payload.jti}` : null;
@@ -1599,15 +1610,15 @@ var TokenService = class TokenService2 {
1599
1610
  const values = await this.getCacheValues(keys);
1600
1611
  const valueByKey = new Map(keys.map((key, i) => [key, values[i]]));
1601
1612
  if (blacklistKey && valueByKey.get(blacklistKey) != null) {
1602
- Err6(this.t("errors.tokenRevoked"));
1613
+ Err6(this.t("errors.tokenRevoked"), 401);
1603
1614
  }
1604
1615
  if (familyKey && valueByKey.get(familyKey) != null) {
1605
- Err6(this.t("errors.tokenRevoked"));
1616
+ Err6(this.t("errors.tokenRevoked"), 401);
1606
1617
  }
1607
1618
  const activeSessionVersion = this.parseSessionVersion(valueByKey.get(sessionKey) ?? null);
1608
1619
  const tokenSessionVersion = payload.sessionVersion ?? 0;
1609
1620
  if (tokenSessionVersion !== activeSessionVersion) {
1610
- Err6(this.t("errors.tokenRevoked"));
1621
+ Err6(this.t("errors.tokenRevoked"), 401);
1611
1622
  }
1612
1623
  return payload;
1613
1624
  }
@@ -1616,13 +1627,13 @@ var TokenService = class TokenService2 {
1616
1627
  try {
1617
1628
  decoded = jwt.verify(token, this.config.jwt.refreshSecret);
1618
1629
  } catch {
1619
- Err6(this.t("errors.tokenVerificationFailed"));
1630
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1620
1631
  }
1621
1632
  if (decoded.type && decoded.type !== "refresh") {
1622
- Err6(this.t("errors.tokenVerificationFailed"));
1633
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1623
1634
  }
1624
1635
  if (!decoded.tokenFamily) {
1625
- Err6(this.t("errors.tokenVerificationFailed"));
1636
+ Err6(this.t("errors.tokenVerificationFailed"), 401);
1626
1637
  }
1627
1638
  return { userId: decoded.userId, tokenFamily: decoded.tokenFamily };
1628
1639
  }
@@ -1641,12 +1652,12 @@ var TokenService = class TokenService2 {
1641
1652
  async resolveRefreshSessionFromCookie() {
1642
1653
  const refreshToken = this.cookieManager.getRefreshToken();
1643
1654
  if (!refreshToken) {
1644
- Err6(this.t("errors.refreshTokenMissing"));
1655
+ Err6(this.t("errors.refreshTokenMissing"), 401);
1645
1656
  }
1646
1657
  const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
1647
1658
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
1648
1659
  if (!stored || stored.userId !== userId) {
1649
- Err6(this.t("errors.refreshTokenInvalid"));
1660
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1650
1661
  }
1651
1662
  const presentedHash = this.hashToken(refreshToken);
1652
1663
  if (presentedHash === stored.token) {
@@ -1656,7 +1667,7 @@ var TokenService = class TokenService2 {
1656
1667
  if (canRecover) {
1657
1668
  return { userId, tokenFamily };
1658
1669
  }
1659
- Err6(this.t("errors.refreshTokenInvalid"));
1670
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1660
1671
  }
1661
1672
  async resolveUserFromCookie() {
1662
1673
  return (await this.resolveRefreshSessionFromCookie()).userId;
@@ -1833,12 +1844,12 @@ var TokenService = class TokenService2 {
1833
1844
  async refreshTokens() {
1834
1845
  const refreshToken = this.cookieManager.getRefreshToken();
1835
1846
  if (!refreshToken) {
1836
- Err6(this.t("errors.refreshTokenMissing"));
1847
+ Err6(this.t("errors.refreshTokenMissing"), 401);
1837
1848
  }
1838
1849
  const { userId, tokenFamily } = this.verifyRefreshToken(refreshToken);
1839
1850
  const stored = await this.tokenRepository.getByFamily(tokenFamily);
1840
1851
  if (!stored || stored.userId !== userId) {
1841
- Err6(this.t("errors.refreshTokenInvalid"));
1852
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1842
1853
  }
1843
1854
  const presentedHash = this.hashToken(refreshToken);
1844
1855
  await this.requireActiveRefreshUser(userId, tokenFamily);
@@ -1849,18 +1860,18 @@ var TokenService = class TokenService2 {
1849
1860
  if (canRecover) {
1850
1861
  const claimed = await this.tokenRepository.markPreviousUsed(tokenFamily, presentedHash);
1851
1862
  if (!claimed?.length) {
1852
- Err6(this.t("errors.refreshTokenInvalid"));
1863
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1853
1864
  }
1854
1865
  return this.generateTokens(userId, tokenFamily);
1855
1866
  }
1856
1867
  await this.revokeSuspectRefreshFamily(userId, tokenFamily);
1857
- Err6(this.t("errors.refreshTokenInvalid"));
1868
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1858
1869
  }
1859
1870
  async requireActiveRefreshUser(userId, tokenFamily) {
1860
1871
  const user = await this.tokenRepository.getUser(userId);
1861
1872
  if (!user || user.status !== "active") {
1862
1873
  await this.revokeFamily(tokenFamily);
1863
- Err6(this.t("errors.refreshTokenInvalid"));
1874
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1864
1875
  }
1865
1876
  return user;
1866
1877
  }
@@ -1894,7 +1905,7 @@ var TokenService = class TokenService2 {
1894
1905
  const userId = await this.resolveUserFromCookie();
1895
1906
  const user = await this.getUserById(userId);
1896
1907
  if (!user) {
1897
- Err6(this.t("errors.refreshTokenInvalid"));
1908
+ Err6(this.t("errors.refreshTokenInvalid"), 401);
1898
1909
  }
1899
1910
  return user;
1900
1911
  }
@@ -2126,6 +2137,27 @@ AuthSessionService = __decorate11([
2126
2137
  __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
2138
  ], AuthSessionService);
2128
2139
 
2140
+ // src/auth/authIdentity.ts
2141
+ var PHONE_PATTERN = /^\+[1-9]\d{7,14}$/;
2142
+ function normalizeAuthIdentifier(value) {
2143
+ if (typeof value !== "string")
2144
+ return null;
2145
+ const trimmed = value.trim();
2146
+ if (!trimmed || trimmed.length > 254)
2147
+ return null;
2148
+ if (trimmed.includes("@")) {
2149
+ return trimmed.toLowerCase();
2150
+ }
2151
+ const compact = trimmed.replace(/[\s().-]+/g, "");
2152
+ const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
2153
+ return PHONE_PATTERN.test(international) ? international : null;
2154
+ }
2155
+ __name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
2156
+ function isEmailIdentifier(value) {
2157
+ return value.includes("@");
2158
+ }
2159
+ __name(isEmailIdentifier, "isEmailIdentifier");
2160
+
2129
2161
  // src/auth/AuthService.ts
2130
2162
  var __decorate12 = function(decorators, target, key, desc) {
2131
2163
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2256,8 +2288,16 @@ var AuthService = class AuthService2 {
2256
2288
  return this.inviteUser(body);
2257
2289
  }
2258
2290
  async loginUser(body) {
2259
- const { email: email2, password } = body;
2260
- const user = await this.userService.findByEmail(email2);
2291
+ const password = body.password;
2292
+ const rawIdentifier = "identifier" in body ? body.identifier : body.email;
2293
+ const identifier = normalizeAuthIdentifier(rawIdentifier);
2294
+ let user;
2295
+ if (identifier && isEmailIdentifier(identifier)) {
2296
+ user = await this.userService.findByEmailInsensitive(identifier);
2297
+ } else if (identifier) {
2298
+ const phoneUser = await this.userService.findByPhone(identifier);
2299
+ user = phoneUser ? await this.userService.findByEmail(phoneUser.email) : void 0;
2300
+ }
2261
2301
  if (user?.lockoutUntil && !this.isLockoutActive(user.lockoutUntil)) {
2262
2302
  await this.userService.resetFailedAttempts(user.id);
2263
2303
  user.failedLoginAttempts = 0;
@@ -2276,10 +2316,10 @@ var AuthService = class AuthService2 {
2276
2316
  Err8(this.t("errors.accountLocked"), 423);
2277
2317
  }
2278
2318
  }
2279
- Err8(this.t("errors.invalidCredentials"));
2319
+ Err8(this.t("errors.invalidCredentials"), 401);
2280
2320
  }
2281
2321
  if (user.status !== "active") {
2282
- Err8(this.t("errors.accountInactive"));
2322
+ Err8(this.t("errors.accountInactive"), 403);
2283
2323
  }
2284
2324
  if (this.config.requireVerifiedEmail && !user.emailVerified) {
2285
2325
  Err8(this.t("errors.emailNotVerified"), 403);
@@ -2406,11 +2446,11 @@ var AuthService = class AuthService2 {
2406
2446
  async changePassword(userId, currentPassword, newPassword) {
2407
2447
  const user = await this.userService.getAuthRecordById(userId);
2408
2448
  if (!user?.password) {
2409
- Err8(this.t("errors.invalidCredentials"));
2449
+ Err8(this.t("errors.invalidCredentials"), 401);
2410
2450
  }
2411
2451
  const isValid = await this.userValidator.comparePassword(currentPassword, user.password);
2412
2452
  if (!isValid) {
2413
- Err8(this.t("errors.invalidCredentials"));
2453
+ Err8(this.t("errors.invalidCredentials"), 401);
2414
2454
  }
2415
2455
  this.userValidator.validatePasswordStrength(newPassword);
2416
2456
  await this.userService.update(userId, { password: newPassword });
@@ -2566,10 +2606,17 @@ var inviteUserDto = z.object({
2566
2606
  var userIdParam = z.object({
2567
2607
  id: z.string().min(1, "User ID is required")
2568
2608
  });
2569
- var loginDto = z.object({
2570
- email: emailField,
2571
- password: passwordField
2572
- });
2609
+ var identifierField = z.string().trim().min(1).max(254);
2610
+ var loginDto = z.union([
2611
+ z.object({
2612
+ identifier: identifierField,
2613
+ password: passwordField
2614
+ }),
2615
+ z.object({
2616
+ email: emailField,
2617
+ password: passwordField
2618
+ })
2619
+ ]);
2573
2620
  var changePasswordDto = z.object({
2574
2621
  currentPassword: passwordField,
2575
2622
  newPassword: passwordField
@@ -2622,19 +2669,19 @@ var cookieFingerprint = /* @__PURE__ */ __name(() => (ctx) => {
2622
2669
  const fingerprint = cookie ? hashKeyPart(cookie) : "none";
2623
2670
  return `${ip}:${fingerprint}`;
2624
2671
  }, "cookieFingerprint");
2625
- var ipAndEmail = /* @__PURE__ */ __name(async (ctx) => {
2672
+ var authIdentityRateLimitKey = /* @__PURE__ */ __name(async (ctx) => {
2626
2673
  const ip = ctx.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? ctx.req.header("x-real-ip") ?? "unknown";
2627
2674
  try {
2628
2675
  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)}`;
2676
+ const identity = body?.identifier ?? body?.email;
2677
+ const normalizedIdentity = normalizeAuthIdentifier(identity);
2678
+ if (normalizedIdentity) {
2679
+ return `${ip}:${hashKeyPart(normalizedIdentity)}`;
2633
2680
  }
2634
2681
  } catch {
2635
2682
  }
2636
2683
  return ip;
2637
- }, "ipAndEmail");
2684
+ }, "authIdentityRateLimitKey");
2638
2685
  var AuthController = class AuthController2 {
2639
2686
  static {
2640
2687
  __name(this, "AuthController");
@@ -2681,7 +2728,7 @@ var AuthController = class AuthController2 {
2681
2728
  };
2682
2729
  __decorate15([
2683
2730
  Post("/register"),
2684
- RateLimit({ limit: 5, window: "15m", key: ipAndEmail }),
2731
+ RateLimit({ limit: 5, window: "15m", key: authIdentityRateLimitKey }),
2685
2732
  Validate(registerDto),
2686
2733
  ResMsg("auth.success.register"),
2687
2734
  __param5(0, Body()),
@@ -2691,7 +2738,7 @@ __decorate15([
2691
2738
  ], AuthController.prototype, "registerUser", null);
2692
2739
  __decorate15([
2693
2740
  Post("/login"),
2694
- RateLimit({ limit: 5, window: "15m", key: ipAndEmail, message: "Too many login attempts. Please try again later." }),
2741
+ RateLimit({ limit: 5, window: "15m", key: authIdentityRateLimitKey, message: "Too many login attempts. Please try again later." }),
2695
2742
  Validate(loginDto),
2696
2743
  ResMsg("auth.success.login"),
2697
2744
  __param5(0, Body()),
@@ -2760,7 +2807,7 @@ __decorate15([
2760
2807
  ], AuthController.prototype, "userProfile", null);
2761
2808
  __decorate15([
2762
2809
  Post("/forgot-password"),
2763
- RateLimit({ limit: 3, window: "15m", key: ipAndEmail, message: "Too many password reset requests. Please try again later." }),
2810
+ RateLimit({ limit: 3, window: "15m", key: authIdentityRateLimitKey, message: "Too many password reset requests. Please try again later." }),
2764
2811
  Validate(resetPasswordDto),
2765
2812
  ResMsg("auth.success.passwordResetSent"),
2766
2813
  __param5(0, Body()),
@@ -5699,6 +5746,7 @@ export {
5699
5746
  assignRoleDto,
5700
5747
  assignRoleParams,
5701
5748
  auth,
5749
+ authIdentityRateLimitKey,
5702
5750
  authSchema,
5703
5751
  authSeed,
5704
5752
  avatarsPath,
@@ -5723,12 +5771,14 @@ export {
5723
5771
  isAdmin,
5724
5772
  isAdministrator,
5725
5773
  isAuth,
5774
+ isEmailIdentifier,
5726
5775
  isEmpty,
5727
5776
  isFile,
5728
5777
  isPath,
5729
5778
  join2 as join,
5730
5779
  languageParam,
5731
5780
  loginDto,
5781
+ normalizeAuthIdentifier,
5732
5782
  oauthAccountsTable,
5733
5783
  own,
5734
5784
  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.11",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [