@jmlq/auth 0.0.1-alpha.15 → 0.0.1-alpha.18

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.
@@ -1,5 +1,5 @@
1
1
  export interface LoginResponse {
2
2
  sessionId: string;
3
3
  accessToken: string;
4
- refreshToken: string;
4
+ refreshToken?: string;
5
5
  }
@@ -10,13 +10,21 @@ class RefreshTokenUseCase {
10
10
  try {
11
11
  // Refrescar la sesión
12
12
  const credential = await this.tokenSession.refreshSession(request.refreshToken);
13
+ // Rotación obligatoria: si no hay refreshToken nuevo, el refresh falló.
14
+ const newRefreshToken = credential.refreshToken;
15
+ if (!newRefreshToken || !newRefreshToken.trim()) {
16
+ // Mensaje genérico por seguridad
17
+ throw new errors_1.TokenExpiredError();
18
+ }
13
19
  return {
14
20
  sessionId: credential.sessionId.getValue(),
15
21
  accessToken: credential.accessToken,
16
- refreshToken: credential.refreshToken,
22
+ refreshToken: newRefreshToken,
17
23
  };
18
24
  }
19
25
  catch (error) {
26
+ // Si quieres, aquí puedes mapear SOLO expiración e invalid-token a TokenExpiredError
27
+ // y relanzar el resto. Mantengo tu comportamiento actual para no cambiar semántica.
20
28
  throw new errors_1.TokenExpiredError();
21
29
  }
22
30
  }
@@ -19,7 +19,7 @@ export declare class Credential {
19
19
  /**
20
20
  * Token de refresco asociado
21
21
  */
22
- private readonly _refreshToken;
22
+ private readonly _refreshToken?;
23
23
  /**
24
24
  * Fecha de expiración del token de acceso
25
25
  */
@@ -48,7 +48,7 @@ export declare class Credential {
48
48
  /**
49
49
  * Obtiene el token de refresco
50
50
  */
51
- get refreshToken(): string;
51
+ get refreshToken(): string | undefined;
52
52
  /**
53
53
  * Obtiene la fecha de expiración del token de acceso
54
54
  */
@@ -73,9 +73,31 @@ export declare class Credential {
73
73
  static create(sessionId: Id, userId: Id, accessToken: string, refreshToken: string, expirationDate: Date): Credential;
74
74
  /**
75
75
  *Reconstitution method for repository
76
- *Método usado por un repositorio para reconstruir la entidad desde la base de datos
77
- * @param props Propiedades de las credenciales
78
- * @returns Nueva instancia de Credential
76
+ *refreshToken puede venir undefined si en DB solo existe hash.
79
77
  */
80
78
  static reconstitute(props: ICredentialProps): Credential;
79
+ /**
80
+ * Crea una nueva credencial como resultado de una rotación de refresh token.
81
+ * Impone que refreshToken exista (rotación obligatoria).
82
+ */
83
+ static rotate(props: {
84
+ sessionId: Id;
85
+ userId: Id;
86
+ accessToken: string;
87
+ refreshToken: string;
88
+ expiresAt: Date;
89
+ createdAt?: Date;
90
+ }): Credential;
91
+ /**
92
+ * Devuelve el refresh token o lanza si no existe.
93
+ *
94
+ * Uso:
95
+ * - Flujos donde el refresh token es OBLIGATORIO
96
+ * (login, refresh/rotación).
97
+ *
98
+ * Seguridad:
99
+ * - Evita usar `!` o cast inseguros.
100
+ * - Hace explícito el invariante del dominio.
101
+ */
102
+ requireRefreshToken(): string;
81
103
  }
@@ -83,12 +83,41 @@ class Credential {
83
83
  }
84
84
  /**
85
85
  *Reconstitution method for repository
86
- *Método usado por un repositorio para reconstruir la entidad desde la base de datos
87
- * @param props Propiedades de las credenciales
88
- * @returns Nueva instancia de Credential
86
+ *refreshToken puede venir undefined si en DB solo existe hash.
89
87
  */
90
88
  static reconstitute(props) {
91
89
  return new Credential(props);
92
90
  }
91
+ /**
92
+ * Crea una nueva credencial como resultado de una rotación de refresh token.
93
+ * Impone que refreshToken exista (rotación obligatoria).
94
+ */
95
+ static rotate(props) {
96
+ return new Credential({
97
+ sessionId: props.sessionId,
98
+ userId: props.userId,
99
+ accessToken: props.accessToken,
100
+ refreshToken: props.refreshToken,
101
+ expiresAt: props.expiresAt,
102
+ createdAt: props.createdAt ?? new Date(),
103
+ });
104
+ }
105
+ /**
106
+ * Devuelve el refresh token o lanza si no existe.
107
+ *
108
+ * Uso:
109
+ * - Flujos donde el refresh token es OBLIGATORIO
110
+ * (login, refresh/rotación).
111
+ *
112
+ * Seguridad:
113
+ * - Evita usar `!` o cast inseguros.
114
+ * - Hace explícito el invariante del dominio.
115
+ */
116
+ requireRefreshToken() {
117
+ if (!this._refreshToken) {
118
+ throw new Error("Invariant violation: refreshToken is required but missing");
119
+ }
120
+ return this._refreshToken;
121
+ }
93
122
  }
94
123
  exports.Credential = Credential;
@@ -45,4 +45,19 @@ export interface ICredentialRepositoryPort {
45
45
  * Elimina una sesión por refresh token (logout basado en refresh).
46
46
  */
47
47
  deleteByRefreshToken(refreshToken: string): Promise<void>;
48
+ /**
49
+ * Rotación atómica de refresh token (single-use).
50
+ *
51
+ * Debe:
52
+ * - “consumir” el refreshToken actual (el entrante) de forma atómica,
53
+ * - y persistir la credencial nueva para la MISMA sessionId.
54
+ *
55
+ * Retorna true si la rotación ocurrió (1 fila afectada),
56
+ * false si el refresh token ya fue usado / revocado / no existe.
57
+ *
58
+ * Nota:
59
+ * - El core NO sabe de hashes.
60
+ * - La implementación infra puede usar refreshTokenHash internamente.
61
+ */
62
+ rotateByRefreshToken(currentRefreshToken: string, nextCredential: Credential): Promise<boolean>;
48
63
  }
@@ -3,7 +3,14 @@ export interface ICredentialProps {
3
3
  sessionId: Id;
4
4
  userId: Id;
5
5
  accessToken: string;
6
- refreshToken: string;
6
+ /**
7
+ * Token de refresco (secreto).
8
+ * Nota de seguridad:
9
+ * - En persistencia NO debe guardarse en claro.
10
+ * - En reconstitución desde DB puede ser undefined (solo existe el hash).
11
+ * - En flujos que emiten tokens (login/refresh) debe existir.
12
+ */
13
+ refreshToken?: string;
7
14
  expiresAt: Date;
8
15
  createdAt: Date;
9
16
  }
@@ -5,6 +5,7 @@ import { Credential, ICredentialRepositoryPort, Id } from "../domain";
5
5
  * Soporta múltiples sesiones por usuario (multi-dispositivo) usando `sessionId`.
6
6
  */
7
7
  export declare class InMemoryCredentialRepository implements ICredentialRepositoryPort {
8
+ rotateByRefreshToken(currentRefreshToken: string, nextCredential: Credential): Promise<boolean>;
8
9
  /**
9
10
  * Fuente de verdad: credenciales indexadas por sessionId.
10
11
  */
@@ -21,6 +21,9 @@ class InMemoryCredentialRepository {
21
21
  */
22
22
  this.refreshTokenIndex = new Map();
23
23
  }
24
+ rotateByRefreshToken(currentRefreshToken, nextCredential) {
25
+ throw new Error("Method not implemented.");
26
+ }
24
27
  /**
25
28
  * Guarda (upsert) una credencial por (userId, sessionId).
26
29
  * - Si existía una credencial para ese sessionId, limpia el índice de refreshToken viejo.
@@ -31,13 +34,14 @@ class InMemoryCredentialRepository {
31
34
  const userKey = credential.userId.getValue();
32
35
  // Si existía una credencial para esa sesión, limpiar refresh index anterior
33
36
  const old = this.credentialsBySessionId.get(sessionKey);
34
- if (old) {
37
+ if (old?.refreshToken) {
35
38
  this.refreshTokenIndex.delete(old.refreshToken);
36
39
  }
37
40
  // Guardar credencial por sesión
38
41
  this.credentialsBySessionId.set(sessionKey, credential);
39
42
  // Indexar refresh -> session
40
- this.refreshTokenIndex.set(credential.refreshToken, sessionKey);
43
+ if (credential.refreshToken)
44
+ this.refreshTokenIndex.set(credential.refreshToken, sessionKey);
41
45
  // Indexar user -> sessions
42
46
  const sessions = this.sessionsByUserId.get(userKey) ?? new Set();
43
47
  sessions.add(sessionKey);
@@ -97,7 +101,7 @@ class InMemoryCredentialRepository {
97
101
  return;
98
102
  for (const sid of sessionIds) {
99
103
  const c = this.credentialsBySessionId.get(sid);
100
- if (c) {
104
+ if (c?.refreshToken) {
101
105
  this.refreshTokenIndex.delete(c.refreshToken);
102
106
  }
103
107
  this.credentialsBySessionId.delete(sid);
@@ -113,7 +117,8 @@ class InMemoryCredentialRepository {
113
117
  if (!c)
114
118
  return;
115
119
  // limpiar refresh index
116
- this.refreshTokenIndex.delete(c.refreshToken);
120
+ if (c.refreshToken)
121
+ this.refreshTokenIndex.delete(c.refreshToken);
117
122
  // limpiar relación user -> sessions
118
123
  const userKey = c.userId.getValue();
119
124
  const sessions = this.sessionsByUserId.get(userKey);
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ // src/infrastructure/services/token-session.service.ts
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.TokenSessionService = void 0;
4
5
  const domain_1 = require("../../domain");
@@ -292,20 +293,15 @@ class TokenSessionService {
292
293
  const { accessToken, refreshToken: newRefreshToken, expiresAt, } = await this.issueTokens(user, sessionId);
293
294
  const rotated = domain_1.Credential.create(sessionId, user.id, accessToken, newRefreshToken, expiresAt);
294
295
  /**
295
- * Estrategia simple (sin sobreingeniería):
296
- * - Borrar por sessionId
297
- * - Guardar credencial rotada
298
- *
299
- * Ventaja:
300
- * - Funciona aunque no exista update/UPSERT
301
- * - Mantiene invariantes del repo simples
302
- *
303
- * Consideración:
304
- * - Si tu storage soporta transacciones, esto puede envolver en una transacción
305
- * para evitar “ventana” de inconsistencia en sistemas altamente concurrentes.
296
+ * Rotación atómica (single-use):
297
+ * - Si otro request ya consumió este refreshToken, esta operación debe fallar.
298
+ * - Evita que el mismo refreshToken se use dos veces bajo concurrencia.
306
299
  */
307
- await this.credentialRepository.deleteBySessionId(sessionId);
308
- await this.credentialRepository.save(rotated);
300
+ const rotatedOk = await this.credentialRepository.rotateByRefreshToken(refreshToken, rotated);
301
+ if (!rotatedOk) {
302
+ // Token ya consumido/revocado/no existe (no filtramos detalle)
303
+ throw new domain_1.TokenExpiredError();
304
+ }
309
305
  return rotated;
310
306
  }
311
307
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jmlq/auth",
3
3
  "description": "JWT authentication package with clean architecture",
4
- "version": "0.0.1-alpha.15",
4
+ "version": "0.0.1-alpha.18",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {