@aranzatech/aranza-auth 0.1.2 → 0.2.1

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 CHANGED
@@ -6,6 +6,44 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-07-03
10
+
11
+ Release de hardening y madurez — incluye fixes de seguridad (0.1.3), hardening profesional y features enterprise (sin OAuth/Prisma).
12
+
13
+ ### Added
14
+
15
+ - **`POST /auth/change-password`**: cambio de contraseña autenticado (invalida refresh tokens).
16
+ - **Hooks con Nest DI**: `useClass` + `ModuleRef.create()` en `forRootAsync`; `hooksProvider` personalizable.
17
+ - **`routePrefix`**: ruta configurable del controller (`forRoot` y `forRootAsync.routePrefix`).
18
+ - **Audit trail Mongo**: `lastLoginAt`, `passwordChangedAt`, `failedLoginAttempts`, `lockedUntil`.
19
+ - **Account lockout** (`features.accountLockout`): bloqueo tras N intentos fallidos.
20
+ - **Refresh theft detection**: refresh reutilizado revoca sesiones (`REFRESH_TOKEN_REUSE`).
21
+ - **Validación al boot**: secrets ≥32 chars, access ≠ refresh, `bcryptRounds` 10–14.
22
+ - **JWT estricto**: algoritmo `HS256` fijado en sign/verify y Passport strategy.
23
+ - **`bcryptRounds`**, **`passwordComplexity`**, **`lockout`** configurables.
24
+ - **`AuthErrorCode`**: códigos exportados (`ACCOUNT_LOCKED`, `REFRESH_TOKEN_REUSE`, etc.).
25
+ - **`AUTH_RATE_LIMIT_PRESETS`**: presets para `@nestjs/throttler`.
26
+ - **SECURITY.md** con tradeoffs y checklist de producción.
27
+ - Índices Mongo en hashes de tokens y fechas de expiración.
28
+ - CI: coverage gate + `npm audit`.
29
+ - **GitHub Releases** automáticos al publicar tags.
30
+
31
+ ### Fixed
32
+
33
+ - **Login timing attack**: siempre ejecuta `bcrypt.compare` (hash dummy si no hay cuenta).
34
+ - **Register user enumeration**: duplicados responden `{ registered: true }`.
35
+ - **`refreshTokenRotation: false`**: refresh valida solo JWT, sin exigir hash en DB.
36
+ - **`JwtStrategy`**: rechaza tokens de cuentas no verificadas con `emailVerification` activo.
37
+ - **`JwtAuthGuard`**: trata `user === false` de Passport como no autenticado (`401`).
38
+ - **`resolveRegisterIdentifier`**: `BadRequestException` en lugar de `Error` genérico.
39
+
40
+ ### Changed
41
+
42
+ - **Register/Login DTOs**: `@IsEmail()` cuando el campo email está presente.
43
+ - Password hashing usa `bcryptRounds` configurable (default 10).
44
+ - `DefaultAuthHooks.enrichMe` incluye `lastLoginAt` y `passwordChangedAt`.
45
+ - Login registra éxito/fallo cuando `accountLockout` está activo.
46
+
9
47
  ## [0.1.2] - 2026-07-03
10
48
 
11
49
  ### Fixed
@@ -33,30 +71,15 @@ First public release.
33
71
  ### Added
34
72
 
35
73
  - **`AuthModule.forRoot` / `forRootAsync`** — módulo dinámico global con configuración JWT.
36
- - **Endpoints REST** bajo `/auth`:
37
- - `POST /auth/register` alta de cuenta
38
- - `POST /auth/login` login con access + refresh token
39
- - `POST /auth/refresh` — renovación de tokens
40
- - `POST /auth/logout` — invalidar refresh token (Bearer)
41
- - `GET /auth/me` usuario autenticado (Bearer)
42
- - `POST /auth/verify-email` — verificación de email (opt-in)
43
- - `POST /auth/forgot-password` — solicitar reset (opt-in)
44
- - `POST /auth/reset-password` — nueva contraseña con token (opt-in)
45
- - **`AuthService`**, **`TokenService`**, **`JwtStrategy`**, **`JwtAuthGuard`**, decorador **`@CurrentUser()`**.
46
- - **Feature flags** (`features`) — todo desactivado por defecto, ideal para POCs:
47
- - `emailVerification` — bloquea login hasta verificar email
48
- - `passwordReset` — habilita forgot/reset password
49
- - `refreshTokenRotation` — rota hash del refresh token en DB (default: `true`)
50
- - **`AuthHooks`** — extiende JWT payload, `/auth/me`, lifecycle y envío de emails.
51
- - **`DefaultAuthHooks`** — implementación mínima incluida.
52
- - **Adapter MongoDB** (`@aranzatech/aranza-auth/mongo`):
53
- - `BaseAuthAccountSchema` — schema base extensible
54
- - `MongoAuthRepository` — implementación de `IAuthRepository`
55
- - `MongoAuthModule.forFeature()` — registro de schema + repository
56
- - Tokens de verificación/reset hasheados (SHA-256) con TTL configurable.
57
- - Build dual ESM/CJS con types (`tsup`), tests (`vitest`), CI GitHub Actions.
58
-
59
- [Unreleased]: https://github.com/aranzatech/aranza-auth/compare/v0.1.2...HEAD
74
+ - **Endpoints REST** bajo `/auth`: register, login, refresh, logout, me, verify-email, forgot/reset password.
75
+ - **`AuthService`**, **`TokenService`**, **`JwtStrategy`**, **`JwtAuthGuard`**, **`@CurrentUser()`**.
76
+ - **Feature flags** (`features`): `emailVerification`, `passwordReset`, `refreshTokenRotation`.
77
+ - **`AuthHooks`** y **`DefaultAuthHooks`**.
78
+ - **Adapter MongoDB** (`@aranzatech/aranza-auth/mongo`).
79
+ - Build dual ESM/CJS, tests (`vitest`), CI GitHub Actions.
80
+
81
+ [Unreleased]: https://github.com/aranzatech/aranza-auth/compare/v0.2.0...HEAD
82
+ [0.2.0]: https://github.com/aranzatech/aranza-auth/compare/v0.1.2...v0.2.0
60
83
  [0.1.2]: https://github.com/aranzatech/aranza-auth/compare/v0.1.1...v0.1.2
61
84
  [0.1.1]: https://github.com/aranzatech/aranza-auth/compare/v0.1.0...v0.1.1
62
85
  [0.1.0]: https://github.com/aranzatech/aranza-auth/releases/tag/v0.1.0
package/README.md CHANGED
@@ -15,6 +15,8 @@ Ideal para reutilizar auth en todos tus proyectos AranzaTech: instalas, configur
15
15
  - [Feature flags](#feature-flags)
16
16
  - [Endpoints](#endpoints)
17
17
  - [Proteger rutas propias](#proteger-rutas-propias)
18
+ - [Rate limiting (producción)](#rate-limiting-producción)
19
+ - [Seguridad en producción](#seguridad-en-producción)
18
20
  - [Extender con AuthHooks](#extender-con-authhooks)
19
21
  - [Extender el schema MongoDB](#extender-el-schema-mongodb)
20
22
  - [Flujos opcionales (email y password)](#flujos-opcionales-email-y-password)
@@ -239,6 +241,9 @@ hooks: AppAuthHooks, // debe implementar sendEmail
239
241
  | `POST` | `/auth/verify-email` | — | `emailVerification` | `{ token }` | `{ verified: true }` |
240
242
  | `POST` | `/auth/forgot-password` | — | `passwordReset` | `{ email }` | `{ sent: true }` |
241
243
  | `POST` | `/auth/reset-password` | — | `passwordReset` | `{ token, newPassword }` | `{ reset: true }` |
244
+ | `POST` | `/auth/change-password` | Bearer | — | `{ currentPassword, newPassword }` | `{ changed: true }` |
245
+
246
+ > Rutas usan `routePrefix` (default `auth`). Ej: `routePrefix: "v1/auth"` → `/v1/auth/login`.
242
247
 
243
248
  ### Errores comunes
244
249
 
@@ -247,9 +252,13 @@ hooks: AppAuthHooks, // debe implementar sendEmail
247
252
  | `401` | `Invalid credentials` | Email/username o password incorrectos |
248
253
  | `401` | `EMAIL_NOT_VERIFIED` | Feature `emailVerification` activa y email sin verificar |
249
254
  | `401` | `ACCOUNT_DISABLED` | Cuenta desactivada |
255
+ | `401` | `ACCOUNT_LOCKED` | Demasiados intentos fallidos (`features.accountLockout`) |
256
+ | `401` | `REFRESH_TOKEN_REUSE` | Refresh reutilizado; sesiones revocadas |
257
+ | `401` | `INVALID_CURRENT_PASSWORD` | Contraseña actual incorrecta en change-password |
250
258
  | `401` | `Invalid refresh token` | Refresh expirado, revocado o inválido |
251
259
  | `404` | — | Feature desactivada (endpoint no disponible) |
252
260
  | `400` | `TOKEN_INVALID_OR_EXPIRED` | Token de verify/reset inválido o expirado |
261
+ | `400` | `PASSWORD_UNCHANGED` | Nueva contraseña igual a la actual |
253
262
 
254
263
  ---
255
264
 
@@ -285,6 +294,93 @@ También puedes registrar `JwtAuthGuard` como guard global:
285
294
 
286
295
  ---
287
296
 
297
+ ## Rate limiting (producción)
298
+
299
+ La librería **no incluye** throttling interno — debes aplicarlo en tu app con `@nestjs/throttler`:
300
+
301
+ ```bash
302
+ npm install @nestjs/throttler
303
+ ```
304
+
305
+ ```typescript
306
+ import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
307
+ import { APP_GUARD } from "@nestjs/core";
308
+ import { AUTH_RATE_LIMIT_PRESETS } from "@aranzatech/aranza-auth";
309
+
310
+ @Module({
311
+ imports: [
312
+ ThrottlerModule.forRoot([
313
+ AUTH_RATE_LIMIT_PRESETS.default,
314
+ AUTH_RATE_LIMIT_PRESETS.credentials,
315
+ AUTH_RATE_LIMIT_PRESETS.passwordReset,
316
+ ]),
317
+ // ...
318
+ ],
319
+ providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
320
+ })
321
+ export class AppModule {}
322
+ ```
323
+
324
+ Presets exportados:
325
+
326
+ | Preset | Uso recomendado | Límite |
327
+ |--------|-----------------|--------|
328
+ | `default` | Rutas auth generales | 10 req/min |
329
+ | `credentials` | `/auth/login`, `/auth/register`, `/auth/refresh` | 5 req/min |
330
+ | `passwordReset` | `/auth/forgot-password` | 3 req/min |
331
+
332
+ > Aplica `@Throttle()` por controlador o ruta según tu política de seguridad.
333
+
334
+ ---
335
+
336
+ ## Seguridad en producción
337
+
338
+ Desde **v0.2.0** la lib valida configuración al arrancar (secrets ≥32 chars, access ≠ refresh).
339
+
340
+ ### Opciones recomendadas
341
+
342
+ ```typescript
343
+ AuthModule.forRootAsync({
344
+ useFactory: (config: ConfigService) => ({
345
+ secret: config.getOrThrow("JWT_SECRET"),
346
+ refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
347
+ expiresIn: "30m", // access corto en prod
348
+ refreshExpiresIn: "7d",
349
+ bcryptRounds: 12, // opcional, default 10
350
+ passwordComplexity: true, // upper + lower + digit
351
+ features: {
352
+ emailVerification: true,
353
+ passwordReset: true,
354
+ refreshTokenRotation: true,
355
+ },
356
+ }),
357
+ });
358
+ ```
359
+
360
+ ### Refresh tokens en cookies (recomendado)
361
+
362
+ ```typescript
363
+ // En login/refresh handler de tu app — no devolver refresh en JSON body
364
+ res.cookie("refreshToken", tokens.refreshToken, {
365
+ httpOnly: true,
366
+ secure: true,
367
+ sameSite: "strict",
368
+ maxAge: 7 * 24 * 60 * 60 * 1000,
369
+ });
370
+ ```
371
+
372
+ ### Códigos de error
373
+
374
+ ```typescript
375
+ import { AuthErrorCode } from "@aranzatech/aranza-auth";
376
+
377
+ // AuthErrorCode.REFRESH_TOKEN_REUSE → posible robo de token; sesiones revocadas
378
+ ```
379
+
380
+ Ver [SECURITY.md](./SECURITY.md) para tradeoffs (logout vs access JWT) y reporte de vulnerabilidades.
381
+
382
+ ---
383
+
288
384
  ## Extender con AuthHooks
289
385
 
290
386
  La lib maneja auth genérico. Tu dominio (org, roles, users) va en **hooks**:
@@ -426,7 +522,7 @@ Si usas `identifierField: "username"`, el register **debe incluir `email`** adem
426
522
 
427
523
  | Import | Contenido |
428
524
  |--------|-----------|
429
- | `@aranzatech/aranza-auth` | `AuthModule`, `AuthService`, guards, DTOs, interfaces, tokens |
525
+ | `@aranzatech/aranza-auth` | `AuthModule`, `AuthService`, guards, DTOs, interfaces, tokens, `AUTH_RATE_LIMIT_PRESETS` |
430
526
  | `@aranzatech/aranza-auth/mongo` | `MongoAuthModule`, `MongoAuthRepository`, schemas |
431
527
 
432
528
  ### Tokens de inyección
@@ -447,27 +543,27 @@ import { AUTH_MODULE_OPTIONS, AUTH_HOOKS, AUTH_REPOSITORY } from "@aranzatech/ar
447
543
  - [ ] MongoDB conectado via `@nestjs/mongoose` (**mongoose@8** recomendado)
448
544
  - [ ] `MongoAuthModule.forFeature()` importado **dentro** de `AuthModule.forRootAsync({ imports })`
449
545
  - [ ] Secrets JWT distintos para access y refresh
546
+ - [ ] **`@nestjs/throttler`** en rutas `/auth/*` (ver [Rate limiting](#rate-limiting-producción))
547
+ - [ ] Access token TTL corto (15–60 min) en producción
450
548
  - [ ] Si usas features de email: implementar `AuthHooks.sendEmail`
451
549
 
452
550
  ---
453
551
 
454
- ## Limitaciones conocidas (v0.1.0)
552
+ ## Limitaciones conocidas (v0.1.x)
455
553
 
456
554
  | Limitación | Workaround / versión futura |
457
555
  |------------|----------------------------|
458
- | Hooks se instancian con `new HooksClass()` sin DI de Nest | Registrar servicios manualmente o v0.2.0 con DI |
459
- | Ruta fija `/auth` (routePrefix en config aún no aplicado) | Usar global prefix de Nest o v0.2.0 |
460
- | Solo adapter MongoDB | Prisma en v0.3.0 |
461
- | Sin OAuth (Google, GitHub, etc.) | v0.2.0 |
462
- | Sin `change-password` autenticado | v0.2.0 |
556
+ | Solo adapter MongoDB | Prisma en v0.4.0 |
557
+ | Sin OAuth (Google, GitHub, etc.) | v0.3.0 |
463
558
 
464
559
  ---
465
560
 
466
561
  ## Roadmap
467
562
 
468
- - [ ] **0.2.0** — OAuth (Google, GitHub), `change-password`, hooks con DI
469
- - [ ] **0.3.0** — Adapter Prisma
470
- - [ ] **0.4.0** — Migración de AranzaFlow como caso de referencia
563
+ - [x] **0.2.0** — Security hardening, `change-password`, hooks con DI, audit trail, account lockout, `routePrefix`
564
+ - [ ] **0.3.0** — OAuth (Google, GitHub)
565
+ - [ ] **0.4.0** — Adapter Prisma
566
+ - [ ] **0.5.0** — Migración de AranzaFlow como caso de referencia
471
567
 
472
568
  ---
473
569
 
package/SECURITY.md ADDED
@@ -0,0 +1,58 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 0.2.x | :white_check_mark: |
8
+ | 0.1.x | :white_check_mark: |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ **Do not open public GitHub issues for security vulnerabilities.**
13
+
14
+ Email: [security@aranzatech.com](mailto:security@aranzatech.com) (or open a private advisory on GitHub if enabled).
15
+
16
+ We aim to respond within **72 hours** and publish a fix or mitigation within **14 days** for confirmed issues.
17
+
18
+ ## Security Model
19
+
20
+ `@aranzatech/aranza-auth` provides authentication primitives. **Your host application** is responsible for:
21
+
22
+ - HTTPS in production
23
+ - Rate limiting (`AUTH_RATE_LIMIT_PRESETS` + `@nestjs/throttler`)
24
+ - Secure storage of refresh tokens (prefer **httpOnly + Secure cookies** over `localStorage`)
25
+ - JWT secrets in a secrets manager (not committed `.env` files)
26
+ - Global `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })`
27
+
28
+ ## Known Tradeoffs
29
+
30
+ | Behavior | Mitigation |
31
+ |----------|------------|
32
+ | **Logout** clears refresh token hash only; access JWT remains valid until expiry | Use short access TTL (15–60 min) |
33
+ | **Register duplicate** returns `{ registered: true }` without revealing existence | By design (anti-enumeration) |
34
+ | **Forgot password** always returns `{ sent: true }` | By design (anti-enumeration) |
35
+ | **Refresh token reuse** revokes all refresh sessions for the account | Monitor `REFRESH_TOKEN_REUSE` errors |
36
+
37
+ ## Hardening Checklist (Production)
38
+
39
+ - [ ] Secrets ≥ 32 characters; access ≠ refresh (validated at boot since v0.2.0)
40
+ - [ ] `features.refreshTokenRotation: true`
41
+ - [ ] `features.emailVerification: true` when using email login
42
+ - [ ] Access token TTL ≤ 60 minutes
43
+ - [ ] `@nestjs/throttler` on all `/auth/*` routes
44
+ - [ ] `passwordComplexity: true` for user-facing apps
45
+ - [ ] Log and alert on `REFRESH_TOKEN_REUSE` responses
46
+
47
+ ## Error Codes
48
+
49
+ | Code | Meaning |
50
+ |------|---------|
51
+ | `REFRESH_TOKEN_REUSE` | Stale refresh token used — all refresh sessions revoked |
52
+ | `ACCOUNT_LOCKED` | Too many failed login attempts |
53
+ | `INVALID_CURRENT_PASSWORD` | Wrong password on change-password |
54
+ | `EMAIL_NOT_VERIFIED` | Login or access blocked until email verified |
55
+ | `ACCOUNT_DISABLED` | Account disabled by admin |
56
+ | `TOKEN_INVALID_OR_EXPIRED` | Email verify or password reset token invalid |
57
+
58
+ Export: `AuthErrorCode` from `@aranzatech/aranza-auth`.
@@ -1,9 +1,13 @@
1
+ import * as _nestjs_common from '@nestjs/common';
2
+
1
3
  interface BaseAuthAccount {
2
4
  id: string;
3
5
  email?: string;
4
6
  username?: string;
5
7
  emailVerified: boolean;
6
8
  disabled: boolean;
9
+ lastLoginAt?: Date;
10
+ passwordChangedAt?: Date;
7
11
  createdAt?: Date;
8
12
  updatedAt?: Date;
9
13
  }
@@ -11,6 +15,8 @@ interface BaseAuthAccount {
11
15
  interface AuthAccountWithSecrets extends BaseAuthAccount {
12
16
  passwordHash?: string;
13
17
  refreshTokenHash?: string | null;
18
+ failedLoginAttempts?: number;
19
+ lockedUntil?: Date | null;
14
20
  }
15
21
  interface RegisterInput {
16
22
  email?: string;
@@ -38,6 +44,14 @@ interface AuthFeatures {
38
44
  passwordReset?: boolean;
39
45
  /** Rotate refresh token hash on login/refresh. Default: `true`. */
40
46
  refreshTokenRotation?: boolean;
47
+ /** Lock account after repeated failed logins. Default: `false`. */
48
+ accountLockout?: boolean;
49
+ }
50
+ interface AuthLockoutOptions {
51
+ /** Max failed attempts before lockout. Default: `5`. */
52
+ maxAttempts?: number;
53
+ /** Lock duration in ms. Default: `15` minutes. */
54
+ lockoutDurationMs?: number;
41
55
  }
42
56
  interface AuthJwtConfig {
43
57
  secret: string;
@@ -56,10 +70,24 @@ interface AuthModuleOptions extends AuthJwtConfig {
56
70
  passwordResetTokenTtlMs?: number;
57
71
  /** Route prefix for auth controller. Default: `auth`. */
58
72
  routePrefix?: string;
59
- /** Custom hooks class. Defaults to built-in minimal payload. */
73
+ /** bcrypt cost factor. Default: `10`. Range: 10–14. */
74
+ bcryptRounds?: number;
75
+ /** Require uppercase, lowercase, and digit on register/reset. Default: `false`. */
76
+ passwordComplexity?: boolean;
77
+ /** Lockout settings when `features.accountLockout` is enabled. */
78
+ lockout?: AuthLockoutOptions;
79
+ /** Custom hooks class (`useClass` — supports Nest DI). Defaults to `DefaultAuthHooks`. */
60
80
  hooks?: new (...args: any[]) => AuthHooks;
81
+ /** Full Nest provider for hooks. Takes precedence over `hooks`. */
82
+ hooksProvider?: _nestjs_common.Provider;
61
83
  }
62
84
  interface AuthModuleAsyncOptions {
85
+ /** Route prefix for auth controller. Default: `auth`. */
86
+ routePrefix?: string;
87
+ /** Custom hooks class (`useClass` with Nest DI via `ModuleRef`). */
88
+ hooks?: new (...args: any[]) => AuthHooks;
89
+ /** Full Nest provider for hooks. Takes precedence over `hooks`. */
90
+ hooksProvider?: _nestjs_common.Provider;
63
91
  imports?: unknown[];
64
92
  inject?: unknown[];
65
93
  useFactory: (...args: unknown[]) => AuthModuleOptions | Promise<AuthModuleOptions>;
@@ -85,6 +113,8 @@ interface IAuthRepository<TAccount extends BaseAuthAccount = BaseAuthAccount> {
85
113
  setResetToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
86
114
  findByResetTokenHash(tokenHash: string): Promise<AuthAccountWithSecrets | null>;
87
115
  clearResetToken(id: string): Promise<void>;
116
+ recordLoginSuccess(id: string): Promise<void>;
117
+ recordLoginFailure(id: string, lockout?: AuthLockoutOptions): Promise<void>;
88
118
  }
89
119
 
90
- export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g };
120
+ export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g, AuthLockoutOptions as h };
@@ -1,9 +1,13 @@
1
+ import * as _nestjs_common from '@nestjs/common';
2
+
1
3
  interface BaseAuthAccount {
2
4
  id: string;
3
5
  email?: string;
4
6
  username?: string;
5
7
  emailVerified: boolean;
6
8
  disabled: boolean;
9
+ lastLoginAt?: Date;
10
+ passwordChangedAt?: Date;
7
11
  createdAt?: Date;
8
12
  updatedAt?: Date;
9
13
  }
@@ -11,6 +15,8 @@ interface BaseAuthAccount {
11
15
  interface AuthAccountWithSecrets extends BaseAuthAccount {
12
16
  passwordHash?: string;
13
17
  refreshTokenHash?: string | null;
18
+ failedLoginAttempts?: number;
19
+ lockedUntil?: Date | null;
14
20
  }
15
21
  interface RegisterInput {
16
22
  email?: string;
@@ -38,6 +44,14 @@ interface AuthFeatures {
38
44
  passwordReset?: boolean;
39
45
  /** Rotate refresh token hash on login/refresh. Default: `true`. */
40
46
  refreshTokenRotation?: boolean;
47
+ /** Lock account after repeated failed logins. Default: `false`. */
48
+ accountLockout?: boolean;
49
+ }
50
+ interface AuthLockoutOptions {
51
+ /** Max failed attempts before lockout. Default: `5`. */
52
+ maxAttempts?: number;
53
+ /** Lock duration in ms. Default: `15` minutes. */
54
+ lockoutDurationMs?: number;
41
55
  }
42
56
  interface AuthJwtConfig {
43
57
  secret: string;
@@ -56,10 +70,24 @@ interface AuthModuleOptions extends AuthJwtConfig {
56
70
  passwordResetTokenTtlMs?: number;
57
71
  /** Route prefix for auth controller. Default: `auth`. */
58
72
  routePrefix?: string;
59
- /** Custom hooks class. Defaults to built-in minimal payload. */
73
+ /** bcrypt cost factor. Default: `10`. Range: 10–14. */
74
+ bcryptRounds?: number;
75
+ /** Require uppercase, lowercase, and digit on register/reset. Default: `false`. */
76
+ passwordComplexity?: boolean;
77
+ /** Lockout settings when `features.accountLockout` is enabled. */
78
+ lockout?: AuthLockoutOptions;
79
+ /** Custom hooks class (`useClass` — supports Nest DI). Defaults to `DefaultAuthHooks`. */
60
80
  hooks?: new (...args: any[]) => AuthHooks;
81
+ /** Full Nest provider for hooks. Takes precedence over `hooks`. */
82
+ hooksProvider?: _nestjs_common.Provider;
61
83
  }
62
84
  interface AuthModuleAsyncOptions {
85
+ /** Route prefix for auth controller. Default: `auth`. */
86
+ routePrefix?: string;
87
+ /** Custom hooks class (`useClass` with Nest DI via `ModuleRef`). */
88
+ hooks?: new (...args: any[]) => AuthHooks;
89
+ /** Full Nest provider for hooks. Takes precedence over `hooks`. */
90
+ hooksProvider?: _nestjs_common.Provider;
63
91
  imports?: unknown[];
64
92
  inject?: unknown[];
65
93
  useFactory: (...args: unknown[]) => AuthModuleOptions | Promise<AuthModuleOptions>;
@@ -85,6 +113,8 @@ interface IAuthRepository<TAccount extends BaseAuthAccount = BaseAuthAccount> {
85
113
  setResetToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
86
114
  findByResetTokenHash(tokenHash: string): Promise<AuthAccountWithSecrets | null>;
87
115
  clearResetToken(id: string): Promise<void>;
116
+ recordLoginSuccess(id: string): Promise<void>;
117
+ recordLoginFailure(id: string, lockout?: AuthLockoutOptions): Promise<void>;
88
118
  }
89
119
 
90
- export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g };
120
+ export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g, AuthLockoutOptions as h };
@@ -1,3 +1,5 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+
1
3
  var __defProp = Object.defineProperty;
2
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
5
  var __decorateClass = (decorators, target, key, kind) => {
@@ -14,15 +16,13 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
14
16
  var AUTH_MODULE_OPTIONS = "AUTH_MODULE_OPTIONS";
15
17
  var AUTH_HOOKS = "AUTH_HOOKS";
16
18
  var AUTH_REPOSITORY = "AUTH_REPOSITORY";
17
-
18
- // src/utils/identifier.util.ts
19
19
  function normalizeIdentifier(value) {
20
20
  return value.trim().toLowerCase();
21
21
  }
22
22
  function resolveRegisterIdentifier(input, field) {
23
23
  const value = field === "email" ? input.email : input.username;
24
24
  if (value == null || value.trim() === "") {
25
- throw new Error(`Register input requires ${field}`);
25
+ throw new BadRequestException(`Register input requires ${field}`);
26
26
  }
27
27
  return normalizeIdentifier(value);
28
28
  }
@@ -32,5 +32,5 @@ function readAccountIdentifier(account, field) {
32
32
  }
33
33
 
34
34
  export { AUTH_HOOKS, AUTH_MODULE_OPTIONS, AUTH_REPOSITORY, __decorateClass, __decorateParam, normalizeIdentifier, readAccountIdentifier, resolveRegisterIdentifier };
35
- //# sourceMappingURL=chunk-JLRBMDLH.js.map
36
- //# sourceMappingURL=chunk-JLRBMDLH.js.map
35
+ //# sourceMappingURL=chunk-QNEFN5ES.js.map
36
+ //# sourceMappingURL=chunk-QNEFN5ES.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants/tokens.ts","../src/utils/identifier.util.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AACO,IAAM,mBAAA,GAAsB;AAC5B,IAAM,UAAA,GAAa;AACnB,IAAM,eAAA,GAAkB;ACExB,SAAS,oBAAoB,KAAA,EAAuB;AACzD,EAAA,OAAO,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAClC;AAEO,SAAS,yBAAA,CACd,OACA,KAAA,EACQ;AACR,EAAA,MAAM,KAAA,GAAQ,KAAA,KAAU,OAAA,GAAU,KAAA,CAAM,QAAQ,KAAA,CAAM,QAAA;AACtD,EAAA,IAAI,KAAA,IAAS,IAAA,IAAQ,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACxC,IAAA,MAAM,IAAI,mBAAA,CAAoB,CAAA,wBAAA,EAA2B,KAAK,CAAA,CAAE,CAAA;AAAA,EAClE;AACA,EAAA,OAAO,oBAAoB,KAAK,CAAA;AAClC;AAEO,SAAS,qBAAA,CACd,SACA,KAAA,EACoB;AACpB,EAAA,MAAM,KAAA,GAAQ,KAAA,KAAU,OAAA,GAAU,OAAA,CAAQ,QAAQ,OAAA,CAAQ,QAAA;AAC1D,EAAA,OAAO,KAAA,IAAS,IAAA,GAAO,mBAAA,CAAoB,KAAK,CAAA,GAAI,MAAA;AACtD","file":"chunk-QNEFN5ES.js","sourcesContent":["/** String tokens — stable across tsup entry points (index + mongo). */\nexport const AUTH_MODULE_OPTIONS = \"AUTH_MODULE_OPTIONS\";\nexport const AUTH_HOOKS = \"AUTH_HOOKS\";\nexport const AUTH_REPOSITORY = \"AUTH_REPOSITORY\";\n","import { BadRequestException } from \"@nestjs/common\";\n\nimport type { AuthIdentifierField } from \"../interfaces/auth-config.interface\";\nimport type { BaseAuthAccount, RegisterInput } from \"../interfaces/auth-hooks.interface\";\n\nexport function normalizeIdentifier(value: string): string {\n return value.trim().toLowerCase();\n}\n\nexport function resolveRegisterIdentifier(\n input: RegisterInput,\n field: AuthIdentifierField,\n): string {\n const value = field === \"email\" ? input.email : input.username;\n if (value == null || value.trim() === \"\") {\n throw new BadRequestException(`Register input requires ${field}`);\n }\n return normalizeIdentifier(value);\n}\n\nexport function readAccountIdentifier(\n account: BaseAuthAccount,\n field: AuthIdentifierField,\n): string | undefined {\n const value = field === \"email\" ? account.email : account.username;\n return value != null ? normalizeIdentifier(value) : undefined;\n}\n"]}