@jmlq/auth 0.0.1-alpha.8 → 0.0.1-beta.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.
Files changed (162) hide show
  1. package/README.es.md +133 -0
  2. package/README.md +81 -207
  3. package/dist/application/dtos/index.d.ts +1 -1
  4. package/dist/application/dtos/index.js +1 -1
  5. package/dist/application/dtos/request/change-password.request.d.ts +15 -0
  6. package/dist/application/dtos/request/index.d.ts +5 -0
  7. package/dist/application/dtos/request/index.js +5 -0
  8. package/dist/application/dtos/request/logout.request.d.ts +2 -1
  9. package/dist/application/dtos/request/me.request.d.ts +3 -0
  10. package/dist/application/dtos/request/me.request.js +2 -0
  11. package/dist/application/dtos/request/register-user.request.d.ts +2 -1
  12. package/dist/application/dtos/request/request-password-reset.request.d.ts +6 -0
  13. package/dist/application/dtos/request/request-password-reset.request.js +2 -0
  14. package/dist/application/dtos/request/reset-password.request.d.ts +14 -0
  15. package/dist/application/dtos/request/reset-password.request.js +2 -0
  16. package/dist/application/dtos/request/verify-email.request.d.ts +3 -0
  17. package/dist/application/dtos/request/verify-email.request.js +2 -0
  18. package/dist/application/dtos/response/change-password.response.d.ts +7 -0
  19. package/dist/application/dtos/response/change-password.response.js +2 -0
  20. package/dist/application/dtos/response/index.d.ts +5 -0
  21. package/dist/application/dtos/response/index.js +5 -0
  22. package/dist/application/dtos/response/login.response.d.ts +2 -1
  23. package/dist/application/dtos/response/me.response.d.ts +11 -0
  24. package/dist/application/dtos/response/me.response.js +2 -0
  25. package/dist/application/dtos/response/refresh-token.response.d.ts +1 -0
  26. package/dist/application/dtos/response/register-user.response.d.ts +9 -0
  27. package/dist/application/dtos/response/request-password-reset.response.d.ts +15 -0
  28. package/dist/application/dtos/response/request-password-reset.response.js +2 -0
  29. package/dist/application/dtos/response/reset-password.response.d.ts +7 -0
  30. package/dist/application/dtos/response/reset-password.response.js +2 -0
  31. package/dist/application/dtos/response/verify-email.response.d.ts +4 -0
  32. package/dist/application/dtos/response/verify-email.response.js +2 -0
  33. package/dist/application/dtos/types/user-role.type.js +2 -0
  34. package/dist/application/facades/auth.facade.d.ts +33 -0
  35. package/dist/application/facades/auth.facade.js +60 -0
  36. package/dist/application/facades/create-auth-facade.d.ts +22 -0
  37. package/dist/application/facades/create-auth-facade.js +9 -0
  38. package/dist/application/facades/index.d.ts +2 -0
  39. package/dist/application/facades/index.js +18 -0
  40. package/dist/application/factories/auth-service.factory.d.ts +4 -4
  41. package/dist/application/factories/auth-service.factory.js +16 -4
  42. package/dist/application/factories/index.js +1 -0
  43. package/dist/application/types/auth-service-factory-options.type.d.ts +44 -0
  44. package/dist/application/use-cases/change-password.use-case.d.ts +21 -0
  45. package/dist/application/use-cases/change-password.use-case.js +49 -0
  46. package/dist/application/use-cases/index.d.ts +5 -0
  47. package/dist/application/use-cases/index.js +5 -0
  48. package/dist/application/use-cases/internal/index.d.ts +1 -0
  49. package/dist/application/use-cases/internal/index.js +17 -0
  50. package/dist/application/use-cases/internal/password-assertions.d.ts +13 -0
  51. package/dist/application/use-cases/internal/password-assertions.js +26 -0
  52. package/dist/application/use-cases/login-with-password.use-case.js +24 -28
  53. package/dist/application/use-cases/logout.use-case.js +14 -2
  54. package/dist/application/use-cases/me.use-case.d.ts +7 -0
  55. package/dist/application/use-cases/me.use-case.js +28 -0
  56. package/dist/application/use-cases/refresh-token.use-case.d.ts +16 -2
  57. package/dist/application/use-cases/refresh-token.use-case.js +33 -5
  58. package/dist/application/use-cases/register-user.use-case.d.ts +6 -1
  59. package/dist/application/use-cases/register-user.use-case.js +18 -7
  60. package/dist/application/use-cases/request-password-reset.use-case.d.ts +19 -0
  61. package/dist/application/use-cases/request-password-reset.use-case.js +43 -0
  62. package/dist/application/use-cases/reset-password.use-case.d.ts +20 -0
  63. package/dist/application/use-cases/reset-password.use-case.js +59 -0
  64. package/dist/application/use-cases/verify-email.use-case.d.ts +8 -0
  65. package/dist/application/use-cases/verify-email.use-case.js +30 -0
  66. package/dist/domain/entities/credential.entity.d.ts +36 -11
  67. package/dist/domain/entities/credential.entity.js +41 -11
  68. package/dist/domain/entities/user.entity.d.ts +32 -1
  69. package/dist/domain/entities/user.entity.js +54 -1
  70. package/dist/domain/errors/auth-error-code.d.ts +16 -0
  71. package/dist/domain/errors/auth-error-code.js +60 -0
  72. package/dist/domain/errors/auth.errors.d.ts +29 -8
  73. package/dist/domain/errors/auth.errors.js +59 -8
  74. package/dist/domain/errors/general.errors.d.ts +4 -0
  75. package/dist/domain/errors/general.errors.js +10 -0
  76. package/dist/domain/errors/identity.errors.d.ts +17 -12
  77. package/dist/domain/errors/identity.errors.js +29 -25
  78. package/dist/domain/errors/index.d.ts +5 -0
  79. package/dist/domain/errors/index.js +5 -0
  80. package/dist/domain/errors/jwt-payload.error.d.ts +4 -0
  81. package/dist/domain/errors/jwt-payload.error.js +10 -0
  82. package/dist/domain/errors/jwt.errors.d.ts +7 -0
  83. package/dist/domain/errors/jwt.errors.js +16 -0
  84. package/dist/domain/errors/password-reset.errors.d.ts +14 -0
  85. package/dist/domain/errors/password-reset.errors.js +29 -0
  86. package/dist/domain/index.d.ts +1 -0
  87. package/dist/domain/index.js +1 -0
  88. package/dist/domain/ports/auth/email-verification-token.port.d.ts +19 -0
  89. package/dist/domain/ports/auth/email-verification-token.port.js +2 -0
  90. package/dist/domain/ports/auth/index.d.ts +2 -0
  91. package/dist/domain/ports/auth/index.js +2 -0
  92. package/dist/domain/ports/auth/password-reset-token.port.d.ts +36 -0
  93. package/dist/domain/ports/auth/password-reset-token.port.js +2 -0
  94. package/dist/domain/ports/jwt/payload/jwt-payload.port.d.ts +25 -3
  95. package/dist/domain/ports/repository/credential.repository.d.ts +55 -2
  96. package/dist/domain/ports/token/token-session.port.d.ts +2 -0
  97. package/dist/domain/props/entities/credential.props.d.ts +9 -1
  98. package/dist/domain/props/entities/user.props.d.ts +1 -0
  99. package/dist/domain/props/jwt/generate-access-token.props.d.ts +3 -2
  100. package/dist/domain/props/jwt/generate-refresh-token.props.d.ts +3 -2
  101. package/dist/domain/props/jwt/jwt-user.d.ts +11 -2
  102. package/dist/domain/services/helpers/assert-jwt-structure.helper.d.ts +1 -0
  103. package/dist/domain/services/helpers/assert-jwt-structure.helper.js +13 -0
  104. package/dist/domain/services/helpers/index.d.ts +7 -0
  105. package/dist/domain/services/helpers/index.js +23 -0
  106. package/dist/domain/services/helpers/optional-audience.helper.d.ts +14 -0
  107. package/dist/domain/services/helpers/optional-audience.helper.js +49 -0
  108. package/dist/domain/services/helpers/optional-non-empty-string.helper.d.ts +1 -0
  109. package/dist/domain/services/helpers/optional-non-empty-string.helper.js +9 -0
  110. package/dist/domain/services/helpers/optional-record.helper.d.ts +1 -0
  111. package/dist/domain/services/helpers/optional-record.helper.js +15 -0
  112. package/dist/domain/services/helpers/optional-roles.helper.d.ts +3 -0
  113. package/dist/domain/services/helpers/optional-roles.helper.js +32 -0
  114. package/dist/domain/services/helpers/require-finite-number.helper.d.ts +1 -0
  115. package/dist/domain/services/helpers/require-finite-number.helper.js +12 -0
  116. package/dist/domain/services/helpers/require-non-empty-string.helper.d.ts +1 -0
  117. package/dist/domain/services/helpers/require-non-empty-string.helper.js +12 -0
  118. package/dist/domain/services/index.d.ts +1 -0
  119. package/dist/domain/services/index.js +1 -0
  120. package/dist/domain/services/normalize-jwt-payload.service.d.ts +19 -0
  121. package/dist/domain/services/normalize-jwt-payload.service.js +58 -0
  122. package/dist/domain/types/access-snapshot.type.d.ts +15 -0
  123. package/dist/domain/types/access-snapshot.type.js +2 -0
  124. package/dist/domain/types/index.d.ts +1 -0
  125. package/dist/domain/types/index.js +2 -0
  126. package/dist/in-memory/in-memory-credential.repository.d.ts +66 -3
  127. package/dist/in-memory/in-memory-credential.repository.js +174 -46
  128. package/dist/index.d.ts +18 -2
  129. package/dist/index.js +24 -9
  130. package/dist/infrastructure/index.d.ts +3 -0
  131. package/dist/infrastructure/index.js +18 -0
  132. package/dist/infrastructure/security/bcrypt-password-hasher.js +0 -1
  133. package/dist/infrastructure/services/token-session.service.d.ts +163 -8
  134. package/dist/infrastructure/services/token-session.service.js +290 -37
  135. package/dist/infrastructure/types/auth-service-container.d.ts +21 -2
  136. package/dist/shared/index.d.ts +1 -0
  137. package/dist/shared/index.js +1 -0
  138. package/dist/shared/jwt-plugin/create-jwt-id.d.ts +6 -0
  139. package/dist/shared/jwt-plugin/create-jwt-id.js +30 -0
  140. package/dist/shared/jwt-plugin/index.d.ts +9 -0
  141. package/dist/shared/jwt-plugin/index.js +25 -0
  142. package/dist/shared/jwt-plugin/is-retryable-auth-code.d.ts +8 -0
  143. package/dist/shared/jwt-plugin/is-retryable-auth-code.js +15 -0
  144. package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.d.ts +14 -0
  145. package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.js +23 -0
  146. package/dist/shared/jwt-plugin/normalize-default-expires-in.d.ts +16 -0
  147. package/dist/shared/jwt-plugin/normalize-default-expires-in.js +36 -0
  148. package/dist/shared/jwt-plugin/read-custom-claims.d.ts +12 -0
  149. package/dist/shared/jwt-plugin/read-custom-claims.js +21 -0
  150. package/dist/shared/jwt-plugin/read-expires-in.d.ts +12 -0
  151. package/dist/shared/jwt-plugin/read-expires-in.js +20 -0
  152. package/dist/shared/jwt-plugin/read-session-id.d.ts +11 -0
  153. package/dist/shared/jwt-plugin/read-session-id.js +17 -0
  154. package/dist/shared/jwt-plugin/resolve-expires-in.d.ts +14 -0
  155. package/dist/shared/jwt-plugin/resolve-expires-in.js +31 -0
  156. package/dist/shared/jwt-plugin/to-date-from-unix-seconds.d.ts +7 -0
  157. package/dist/shared/jwt-plugin/to-date-from-unix-seconds.js +12 -0
  158. package/package.json +12 -11
  159. /package/dist/application/dtos/{type/user-role.type.js → request/change-password.request.js} +0 -0
  160. /package/dist/application/dtos/{type → types}/index.d.ts +0 -0
  161. /package/dist/application/dtos/{type → types}/index.js +0 -0
  162. /package/dist/application/dtos/{type → types}/user-role.type.d.ts +0 -0
@@ -1,87 +1,340 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TokenSessionService = void 0;
4
- // src/infrastructure/services/token-session.service.ts
5
4
  const domain_1 = require("../../domain");
6
5
  /**
7
- * Servicio de sesiones de usuario (rotación de refresh token).
6
+ * Extrae el `sid` (sessionId) del payload de forma segura.
8
7
  *
9
- * Este servicio NO debe conocer detalles crypto.
10
- * Solo orquesta:
11
- * - generación/verificación de tokens vía ITokenService
12
- * - persistencia de credenciales
13
- * - recuperación de usuario
8
+ * Nota:
9
+ * - En la mayoría de implementaciones, `sid` viene como claim plano.
10
+ * - Si tu `ITokenServicePort` lo entrega de otra forma, ajusta aquí (único lugar).
11
+ *
12
+ * Ventaja:
13
+ * - Centraliza la dependencia en el formato del payload (single source of truth).
14
+ *
15
+ * @param payload Payload verificado del JWT.
16
+ * @returns sessionId (sid).
17
+ */
18
+ function getSessionIdFromPayload(payload) {
19
+ // Aquí se asume que IJWTPayload expone "sid" como string.
20
+ // Si llegara a ser opcional en el tipo, este método debería retornar string | undefined
21
+ // y el resto del flujo ya está preparado para manejar "falsy".
22
+ return payload.sid;
23
+ }
24
+ /**
25
+ * Servicio de sesiones de usuario (rotación de refresh token) con soporte multi-dispositivo.
26
+ *
27
+ * Capa:
28
+ * - Infraestructura (orquesta ports y persistencia, no contiene reglas de negocio de Auth)
29
+ *
30
+ * Responsabilidades:
31
+ * - Generar/verificar tokens usando ITokenServicePort (plugin: jose / etc.)
32
+ * - Persistir credenciales por sessionId (1 fila = 1 sesión/dispositivo)
33
+ * - Recuperar usuario desde IUserRepositoryPort
34
+ *
35
+ * Reglas clave (multi-dispositivo):
36
+ * - createSession: crea sessionId nuevo (nuevo dispositivo/sesión)
37
+ * - refreshSession: rota tokens manteniendo el MISMO sessionId (misma sesión)
38
+ * - validateSession: valida JWT + valida que la sesión exista (revocable) y no expirada
14
39
  */
15
40
  class TokenSessionService {
16
- constructor(tokenService, userRepository, credentialRepository,
17
- // Expiraciones humanas opcionales (si las quieres como string)
18
- accessTokenExpiration = "15m", refreshTokenExpiration = "7d") {
41
+ constructor(
42
+ /**
43
+ * Puerto para operaciones JWT (generar/verificar/expiración).
44
+ * Implementación típica: jose (via @jmlq/auth-plugin-jose).
45
+ */
46
+ tokenService,
47
+ /**
48
+ * Puerto de usuarios (fuente de verdad del usuario).
49
+ * Se usa para:
50
+ * - encontrar usuario por id (payload.sub)
51
+ * - validar canLogin()
52
+ */
53
+ userRepository,
54
+ /**
55
+ * Puerto de credenciales/sesiones (persistencia revocable).
56
+ * Se usa para:
57
+ * - save / findBySessionId / findByRefreshToken
58
+ * - deleteByRefreshToken / deleteBySessionId
59
+ */
60
+ credentialRepository,
61
+ /**
62
+ * Expiración del access token en formato humano.
63
+ * @example "15m"
64
+ *
65
+ * Nota:
66
+ * - La interpretación la hace el tokenService (o un parser interno).
67
+ */
68
+ accessTokenExpiration = "15m",
69
+ /**
70
+ * Expiración del refresh token en formato humano.
71
+ * @example "7d"
72
+ */
73
+ refreshTokenExpiration = "7d") {
19
74
  this.tokenService = tokenService;
20
75
  this.userRepository = userRepository;
21
76
  this.credentialRepository = credentialRepository;
22
77
  this.accessTokenExpiration = accessTokenExpiration;
23
78
  this.refreshTokenExpiration = refreshTokenExpiration;
24
79
  }
25
- // Crear una nueva sesión
26
- async createSession(user) {
80
+ /**
81
+ * Emite access/refresh tokens para un usuario y una sesión dada.
82
+ *
83
+ * Este método es el “núcleo” de emisión de tokens y su claim policy.
84
+ *
85
+ * Qué hace:
86
+ * 1) Construye claims del usuario (id/email/roles)
87
+ * 2) Deriva permissions efectivas desde roles (RBAC)
88
+ * 3) Construye customClaims.permissions para que el API pueda autorizar (requirePermissions)
89
+ * 4) Genera access + refresh
90
+ * 5) Obtiene expiresAt desde el accessToken (fuente robusta: tokenService)
91
+ *
92
+ * Importante:
93
+ * - permissions vienen de roles.getValuePublic().permissions
94
+ * (en el host se “pegan” desde AccessSnapshotResolver)
95
+ *
96
+ * @param user Usuario autenticado.
97
+ * @param sessionId Identificador de sesión/dispositivo.
98
+ */
99
+ async issueTokens(user, sessionId) {
100
+ /**
101
+ * Política:
102
+ * - JWT delgado: NO roles, NO permissions.
103
+ * - Authorization se resuelve en el host (BDD) por middleware/servicio.
104
+ */
105
+ const userClaims = {
106
+ id: user.id.getValue(),
107
+ // email/roles son opcionales y NO se incluyen por defecto.
108
+ };
109
+ // Custom claims: no forzamos nada desde el core.
110
+ // Si el host quiere customClaims, debe hacerlo en su propio flujo/política.
111
+ const customClaims = {};
112
+ /**
113
+ * Generar access token:
114
+ * - sessionId se coloca en "sid"
115
+ * - expiresIn aplica política de expiración configurada
116
+ * - customClaims adjunta permissions para autorización en el API
117
+ */
27
118
  const accessToken = await this.tokenService.generateAccessToken({
28
- user: {
29
- id: user.id.toString(),
30
- email: user.email.toString(),
31
- roles: user.roles.map((r) => r.getValuePublic()),
32
- },
119
+ user: userClaims,
33
120
  expiresIn: this.accessTokenExpiration,
121
+ sessionId: sessionId.getValue(),
122
+ customClaims,
34
123
  });
124
+ /**
125
+ * Generar refresh token:
126
+ * - Mantiene mismo sid
127
+ * - Suele tener expiración mayor
128
+ * - También transporta customClaims (depende de tu política; aquí se incluye por consistencia)
129
+ */
35
130
  const refreshToken = await this.tokenService.generateRefreshToken({
36
- user: {
37
- id: user.id.toString(),
38
- email: user.email.toString(),
39
- roles: user.roles.map((r) => r.getValuePublic()),
40
- },
131
+ user: userClaims,
41
132
  expiresIn: this.refreshTokenExpiration,
133
+ sessionId: sessionId.getValue(),
134
+ customClaims,
42
135
  });
43
- // Obtener expiración desde el token (más robusto)
136
+ /**
137
+ * Expiración del access token:
138
+ * - Se deriva desde el token (tokenService), evitando parseos propios
139
+ * - Reduce riesgo de inconsistencias entre "exp" y cálculos internos
140
+ */
44
141
  const expiresAt = await this.tokenService.getTokenExpiration(accessToken);
45
- const credential = domain_1.Credential.create(user.id, accessToken, refreshToken, expiresAt);
142
+ return { accessToken, refreshToken, expiresAt };
143
+ }
144
+ /**
145
+ * Crea una nueva sesión (nuevo dispositivo).
146
+ *
147
+ * Flujo:
148
+ * 1) Genera un sessionId nuevo
149
+ * 2) Emite tokens atados a ese sessionId
150
+ * 3) Construye Credential (entidad de dominio de sesión)
151
+ * 4) Persiste credencial
152
+ *
153
+ * Efecto:
154
+ * - multi-dispositivo: un usuario puede tener N credenciales activas (N sessionId)
155
+ *
156
+ * @param user Usuario autenticado.
157
+ * @returns Credencial persistida para la nueva sesión.
158
+ */
159
+ async createSession(user) {
160
+ const sessionId = domain_1.Id.generate();
161
+ const { accessToken, refreshToken, expiresAt } = await this.issueTokens(user, sessionId);
162
+ /**
163
+ * Credential representa la sesión activa.
164
+ * Suele incluir:
165
+ * - sessionId
166
+ * - userId
167
+ * - accessToken actual
168
+ * - refreshToken actual
169
+ * - expiresAt (del access)
170
+ */
171
+ const credential = domain_1.Credential.create(sessionId, user.id, accessToken, refreshToken, expiresAt);
46
172
  await this.credentialRepository.save(credential);
47
173
  return credential;
48
174
  }
49
- // Rotación de refresh token
175
+ /**
176
+ * Rota refresh token manteniendo el MISMO sessionId (misma sesión/dispositivo).
177
+ *
178
+ * Flujo:
179
+ * 1) Busca credencial por refreshToken (revocable)
180
+ * 2) Verifica refreshToken (firma/exp)
181
+ * 3) Recupera usuario (payload.sub) y valida canLogin()
182
+ * 4) Re-emite tokens con el MISMO sessionId
183
+ * 5) Reemplaza credencial persistida (delete + save)
184
+ *
185
+ * Seguridad:
186
+ * - Si el refreshToken no existe en repositorio => sesión revocada o token inválido => TokenExpiredError
187
+ * - Verificación JWT también puede lanzar => TokenExpiredError
188
+ *
189
+ * @param refreshToken Refresh token actual.
190
+ * @returns Nueva credencial rotada para la misma sesión.
191
+ */
50
192
  async refreshSession(refreshToken) {
51
193
  const existing = await this.credentialRepository.findByRefreshToken(refreshToken);
194
+ // Si no existe el refreshToken en DB, se considera expirado/revocado
52
195
  if (!existing)
53
- throw new domain_1.InvalidOrExpiredRefreshTokenError();
54
- // Verifica refresh token (firma + exp + issuer/aud si están configurados)
196
+ throw new domain_1.TokenExpiredError();
197
+ /**
198
+ * Verificación criptográfica y claims:
199
+ * - Si la firma o exp fallan => TokenExpiredError
200
+ *
201
+ * Nota:
202
+ * - Se oculta el error real por seguridad (no filtrar detalles al cliente).
203
+ */
55
204
  const payload = await this.tokenService
56
205
  .verifyRefreshToken(refreshToken)
57
206
  .catch(() => {
58
- throw new domain_1.InvalidOrExpiredRefreshTokenError();
207
+ throw new domain_1.TokenExpiredError();
59
208
  });
209
+ /**
210
+ * El sub del payload identifica al usuario.
211
+ * Se re-carga para:
212
+ * - validar existencia
213
+ * - validar estado (canLogin)
214
+ * - obtener roles/permisos actuales (si el repo resuelve RBAC en cada fetch)
215
+ */
60
216
  const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
61
217
  if (!user)
62
218
  throw new domain_1.UserNotFoundError();
63
219
  if (!user.canLogin())
64
220
  throw new domain_1.UserDisabledError();
65
- // Crea nueva sesión
66
- const newCredential = await this.createSession(user);
67
- // Borra credencial anterior (rotación)
68
- await this.credentialRepository.deleteByRefreshToken(refreshToken);
69
- return newCredential;
221
+ /**
222
+ * Mantener identidad de sesión:
223
+ * - NO se crea un sessionId nuevo
224
+ * - Se rota token, pero el dispositivo/sesión sigue siendo el mismo
225
+ */
226
+ const sessionId = existing.sessionId;
227
+ const { accessToken, refreshToken: newRefreshToken, expiresAt, } = await this.issueTokens(user, sessionId);
228
+ const rotated = domain_1.Credential.create(sessionId, user.id, accessToken, newRefreshToken, expiresAt);
229
+ /**
230
+ * Rotación atómica (single-use):
231
+ * - Si otro request ya consumió este refreshToken, esta operación debe fallar.
232
+ * - Evita que el mismo refreshToken se use dos veces bajo concurrencia.
233
+ */
234
+ const rotatedOk = await this.credentialRepository.rotateByRefreshToken(refreshToken, rotated);
235
+ if (!rotatedOk) {
236
+ // Token ya consumido/revocado/no existe (no filtramos detalle)
237
+ throw new domain_1.TokenExpiredError();
238
+ }
239
+ return rotated;
70
240
  }
71
- // Validación de sesión via access token
241
+ /**
242
+ * Valida una sesión a partir de access token.
243
+ *
244
+ * Este método soporta “revocación” (token válido criptográficamente pero sesión eliminada en DB).
245
+ *
246
+ * Reglas:
247
+ * - JWT debe ser válido (firma/exp/issuer/aud según plugin)
248
+ * - Debe existir `sid` en el payload
249
+ * - La sesión (sid) debe existir en el repositorio
250
+ * - (recomendado) accessToken debe coincidir con el almacenado (solo el último token es válido)
251
+ * - La credencial no debe estar expirada (según su estado interno)
252
+ * - El usuario debe existir y poder loguearse
253
+ *
254
+ * @param accessToken Access token recibido.
255
+ * @returns Usuario si sesión es válida, o null si no lo es.
256
+ */
72
257
  async validateSession(accessToken) {
73
258
  try {
259
+ /**
260
+ * verifyAccessToken:
261
+ * - valida firma y exp y claims críticos según configuración del tokenService
262
+ * - retorna payload tipado como IJWTPayload
263
+ */
74
264
  const payload = await this.tokenService.verifyAccessToken(accessToken);
265
+ /**
266
+ * sid:
267
+ * - identifica la sesión/dispositivo
268
+ * - si no existe, no podemos validar sesión revocable => null
269
+ */
270
+ const sid = getSessionIdFromPayload(payload);
271
+ if (!sid)
272
+ return null;
273
+ /**
274
+ * Validación de sesión en storage:
275
+ * - si no existe => sesión revocada => null
276
+ */
277
+ const credential = await this.credentialRepository.findBySessionId(new domain_1.Id(sid));
278
+ if (!credential)
279
+ return null;
280
+ /**
281
+ * Validación recomendada:
282
+ * - asegura que solo el último accessToken emitido para esa sesión sea aceptado
283
+ * - previene replay de accessTokens antiguos dentro de la misma sesión
284
+ *
285
+ * Nota:
286
+ * - Esto exige que el repo persista accessToken actual.
287
+ */
288
+ if (credential.accessToken !== accessToken)
289
+ return null;
290
+ /**
291
+ * Validación de expiración a nivel de credencial:
292
+ * - respaldo adicional (además de "exp" del JWT)
293
+ * - útil si tu sistema modela expiración o revocación interna
294
+ */
295
+ if (credential.isExpired())
296
+ return null;
297
+ /**
298
+ * sub del payload identifica al usuario.
299
+ * Se valida existencia y estado.
300
+ */
75
301
  const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
76
302
  return user && user.canLogin() ? user : null;
77
303
  }
78
- catch {
79
- return null;
304
+ catch (e) {
305
+ /**
306
+ * SessionAuthError:
307
+ * - encapsula el error interno (logging/observabilidad)
308
+ * - evita filtrar detalles sensibles al exterior
309
+ */
310
+ throw new domain_1.SessionAuthError("Session Authentication failed", e);
80
311
  }
81
312
  }
82
- // Revocar sesión por refresh token
313
+ /**
314
+ * Revoca una sesión usando refresh token.
315
+ *
316
+ * Uso típico:
317
+ * - logout donde el cliente envía refreshToken (cookie/body)
318
+ *
319
+ * Efecto:
320
+ * - la sesión deja de poder refrescar tokens
321
+ * - si validateSession compara accessToken, también invalida el access actual cuando se borra la credencial
322
+ *
323
+ * @param refreshToken Refresh token actual.
324
+ */
83
325
  async revokeSession(refreshToken) {
84
326
  await this.credentialRepository.deleteByRefreshToken(refreshToken);
85
327
  }
328
+ /**
329
+ * Revoca una sesión por sessionId (logout por dispositivo).
330
+ *
331
+ * Uso típico:
332
+ * - logout selectivo (cerrar solo este dispositivo)
333
+ *
334
+ * @param sessionId Identificador de sesión/dispositivo.
335
+ */
336
+ async revokeSessionById(sessionId) {
337
+ await this.credentialRepository.deleteBySessionId(sessionId);
338
+ }
86
339
  }
87
340
  exports.TokenSessionService = TokenSessionService;
@@ -1,5 +1,10 @@
1
- import { ICredentialRepositoryPort, IPasswordHasherPort, IPasswordPolicyPort, ITokenServicePort, ITokenSessionPort, IUserRepositoryPort } from "../../domain/ports";
2
- import { LoginWithPasswordUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUserUseCase } from "../../application/use-cases";
1
+ import { ICredentialRepositoryPort, IPasswordHasherPort, IPasswordPolicyPort, ITokenServicePort, ITokenSessionPort, IUserRepositoryPort, IPasswordResetTokenPort, IEmailVerificationTokenPort } from "../../domain/ports";
2
+ import { ChangePasswordUseCase, LoginWithPasswordUseCase, LogoutUseCase, MeUseCase, RefreshTokenUseCase, RegisterUserUseCase, RequestPasswordResetUseCase, ResetPasswordUseCase, VerifyEmailUseCase } from "../../application/use-cases";
3
+ /**
4
+ * IAuthServiceContainer es el punto de composición del módulo de autenticación
5
+ * Es un contrato que agrupa todas las dependencias y casos de uso de Auth ya construidos,
6
+ * listos para ser consumidos por una aplicación (API REST, GraphQL, CLI, etc.).
7
+ */
3
8
  export interface IAuthServiceContainer {
4
9
  userRepository: IUserRepositoryPort;
5
10
  credentialRepository: ICredentialRepositoryPort;
@@ -7,8 +12,22 @@ export interface IAuthServiceContainer {
7
12
  tokenService: ITokenServicePort;
8
13
  passwordPolicy: IPasswordPolicyPort;
9
14
  tokenSession: ITokenSessionPort;
15
+ passwordResetToken: IPasswordResetTokenPort;
16
+ /**
17
+ * Puerto para la gestión de tokens de verificación de email.
18
+ *
19
+ * Responsabilidad:
20
+ * - Emitir tokens temporales (single-use) asociados a un usuario.
21
+ * - Validar y consumir el token cuando el usuario accede al enlace de verificación.
22
+ */
23
+ emailVerificationToken: IEmailVerificationTokenPort;
10
24
  registerUserUseCase: RegisterUserUseCase;
11
25
  loginWithPasswordUseCase: LoginWithPasswordUseCase;
12
26
  refreshTokenUseCase: RefreshTokenUseCase;
13
27
  logoutUseCase: LogoutUseCase;
28
+ requestPasswordResetUseCase: RequestPasswordResetUseCase;
29
+ resetPasswordUseCase: ResetPasswordUseCase;
30
+ changePasswordUseCase: ChangePasswordUseCase;
31
+ verifyEmailUseCase: VerifyEmailUseCase;
32
+ meUseCase: MeUseCase;
14
33
  }
@@ -1 +1,2 @@
1
1
  export * from "./utils";
2
+ export * from "./jwt-plugin";
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./utils"), exports);
18
+ __exportStar(require("./jwt-plugin"), exports);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Genera un identificador único para el claim `jti`.
3
+ * - Usa crypto.randomUUID cuando está disponible.
4
+ * - Fallback no-crypto para entornos legacy/dev.
5
+ */
6
+ export declare function createJwtId(): string;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createJwtId = createJwtId;
4
+ /**
5
+ * Obtiene el tiempo actual en segundos Unix.
6
+ *
7
+ * @returns Timestamp Unix (segundos).
8
+ */
9
+ function nowUnixSeconds() {
10
+ return Math.floor(Date.now() / 1000);
11
+ }
12
+ /**
13
+ * Genera un identificador único para el claim `jti`.
14
+ * - Usa crypto.randomUUID cuando está disponible.
15
+ * - Fallback no-crypto para entornos legacy/dev.
16
+ */
17
+ function createJwtId() {
18
+ if (hasCryptoRandomUUID(globalThis)) {
19
+ return globalThis.crypto.randomUUID();
20
+ }
21
+ return `jti_${nowUnixSeconds()}_${Math.random().toString(16).slice(2)}`;
22
+ }
23
+ function hasCryptoRandomUUID(value) {
24
+ return (typeof value === "object" &&
25
+ value !== null &&
26
+ "crypto" in value &&
27
+ typeof value.crypto === "object" &&
28
+ typeof value.crypto
29
+ ?.randomUUID === "function");
30
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./normalize-clock-skew-seconds";
2
+ export * from "./normalize-default-expires-in";
3
+ export * from "./read-expires-in";
4
+ export * from "./read-custom-claims";
5
+ export * from "./resolve-expires-in";
6
+ export * from "./is-retryable-auth-code";
7
+ export * from "./read-session-id";
8
+ export * from "./to-date-from-unix-seconds";
9
+ export * from "./create-jwt-id";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./normalize-clock-skew-seconds"), exports);
18
+ __exportStar(require("./normalize-default-expires-in"), exports);
19
+ __exportStar(require("./read-expires-in"), exports);
20
+ __exportStar(require("./read-custom-claims"), exports);
21
+ __exportStar(require("./resolve-expires-in"), exports);
22
+ __exportStar(require("./is-retryable-auth-code"), exports);
23
+ __exportStar(require("./read-session-id"), exports);
24
+ __exportStar(require("./to-date-from-unix-seconds"), exports);
25
+ __exportStar(require("./create-jwt-id"), exports);
@@ -0,0 +1,8 @@
1
+ import type { AuthErrorCode } from "../../domain/errors";
2
+ /**
3
+ * Determina si un error del core (por código) es "retryable".
4
+ *
5
+ * Responsabilidad única:
6
+ * - Decidir reintento SOLO por `code` (sin jose, sin message, sin heurísticas).
7
+ */
8
+ export declare function isRetryableAuthCode(code: AuthErrorCode): boolean;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRetryableAuthCode = isRetryableAuthCode;
4
+ /**
5
+ * Determina si un error del core (por código) es "retryable".
6
+ *
7
+ * Responsabilidad única:
8
+ * - Decidir reintento SOLO por `code` (sin jose, sin message, sin heurísticas).
9
+ */
10
+ function isRetryableAuthCode(code) {
11
+ return (code === "SIGNATURE_INVALID" ||
12
+ code === "ALGORITHM_UNSUPPORTED" ||
13
+ code === "KEY_MISMATCH" ||
14
+ code === "KEY_NOT_FOUND");
15
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normaliza clockSkewSeconds (en segundos).
3
+ *
4
+ * Responsabilidad única:
5
+ * - Aceptar únicamente números válidos
6
+ * - Convertir a entero (floor)
7
+ * - Asegurar >= 0
8
+ *
9
+ * Reglas:
10
+ * - no number / NaN => undefined
11
+ * - < 0 => 0
12
+ * - >== 0 => floor(value)
13
+ */
14
+ export declare function normalizeClockSkewSeconds(value: number | undefined): number | undefined;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeClockSkewSeconds = normalizeClockSkewSeconds;
4
+ /**
5
+ * Normaliza clockSkewSeconds (en segundos).
6
+ *
7
+ * Responsabilidad única:
8
+ * - Aceptar únicamente números válidos
9
+ * - Convertir a entero (floor)
10
+ * - Asegurar >= 0
11
+ *
12
+ * Reglas:
13
+ * - no number / NaN => undefined
14
+ * - < 0 => 0
15
+ * - >== 0 => floor(value)
16
+ */
17
+ function normalizeClockSkewSeconds(value) {
18
+ if (typeof value !== "number" || Number.isNaN(value))
19
+ return undefined;
20
+ if (value < 0)
21
+ return 0;
22
+ return Math.floor(value);
23
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Normaliza defaults de expiración usados por plugins JWT.
3
+ *
4
+ * Responsabilidad única:
5
+ * - Aceptar un shape compatible con { accessToken?, refreshToken? }
6
+ * - Trim de strings
7
+ * - Vacío => omitido
8
+ * - Si queda vacío => undefined
9
+ *
10
+ * Importante:
11
+ * - No depende de types de plugins para evitar acoplamiento.
12
+ */
13
+ export declare function normalizeDefaultExpiresIn<T extends {
14
+ accessToken?: string;
15
+ refreshToken?: string;
16
+ }>(value: T | undefined): T | undefined;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeDefaultExpiresIn = normalizeDefaultExpiresIn;
4
+ /**
5
+ * Normaliza defaults de expiración usados por plugins JWT.
6
+ *
7
+ * Responsabilidad única:
8
+ * - Aceptar un shape compatible con { accessToken?, refreshToken? }
9
+ * - Trim de strings
10
+ * - Vacío => omitido
11
+ * - Si queda vacío => undefined
12
+ *
13
+ * Importante:
14
+ * - No depende de types de plugins para evitar acoplamiento.
15
+ */
16
+ function normalizeDefaultExpiresIn(value) {
17
+ if (!value)
18
+ return undefined;
19
+ const out = {};
20
+ const accessToken = normalizeOptionalNonEmptyString(value.accessToken);
21
+ if (accessToken)
22
+ out.accessToken = accessToken;
23
+ const refreshToken = normalizeOptionalNonEmptyString(value.refreshToken);
24
+ if (refreshToken)
25
+ out.refreshToken = refreshToken;
26
+ return Object.keys(out).length > 0 ? out : undefined;
27
+ }
28
+ /**
29
+ * Helper local (mínimo) para evitar dependencia circular en exports del core.
30
+ */
31
+ function normalizeOptionalNonEmptyString(value) {
32
+ if (typeof value !== "string")
33
+ return undefined;
34
+ const v = value.trim();
35
+ return v.length > 0 ? v : undefined;
36
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Lee `customClaims` desde unknown.
3
+ *
4
+ * Responsabilidad única:
5
+ * - Aceptar únicamente un objeto plano serializable (Record<string, unknown>)
6
+ *
7
+ * Reglas:
8
+ * - undefined/null => undefined
9
+ * - arrays => undefined
10
+ * - objetos => Record<string, unknown>
11
+ */
12
+ export declare function readCustomClaims(value: unknown): Record<string, unknown> | undefined;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readCustomClaims = readCustomClaims;
4
+ /**
5
+ * Lee `customClaims` desde unknown.
6
+ *
7
+ * Responsabilidad única:
8
+ * - Aceptar únicamente un objeto plano serializable (Record<string, unknown>)
9
+ *
10
+ * Reglas:
11
+ * - undefined/null => undefined
12
+ * - arrays => undefined
13
+ * - objetos => Record<string, unknown>
14
+ */
15
+ function readCustomClaims(value) {
16
+ if (!value || typeof value !== "object")
17
+ return undefined;
18
+ if (Array.isArray(value))
19
+ return undefined;
20
+ return value;
21
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Lee `expiresIn` desde unknown.
3
+ *
4
+ * Responsabilidad única:
5
+ * - Normalizar un valor desconocido a `string | undefined`
6
+ *
7
+ * Reglas:
8
+ * - no string => undefined
9
+ * - string vacío => undefined
10
+ * - string con espacios => trim()
11
+ */
12
+ export declare function readExpiresIn(value: unknown): string | undefined;