@jmlq/auth 0.0.1-alpha.9 → 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.
- package/README.es.md +133 -0
- package/README.md +81 -207
- package/dist/application/dtos/index.d.ts +1 -1
- package/dist/application/dtos/index.js +1 -1
- package/dist/application/dtos/request/change-password.request.d.ts +15 -0
- package/dist/application/dtos/request/index.d.ts +5 -0
- package/dist/application/dtos/request/index.js +5 -0
- package/dist/application/dtos/request/logout.request.d.ts +2 -1
- package/dist/application/dtos/request/me.request.d.ts +3 -0
- package/dist/application/dtos/request/me.request.js +2 -0
- package/dist/application/dtos/request/register-user.request.d.ts +2 -1
- package/dist/application/dtos/request/request-password-reset.request.d.ts +6 -0
- package/dist/application/dtos/request/request-password-reset.request.js +2 -0
- package/dist/application/dtos/request/reset-password.request.d.ts +14 -0
- package/dist/application/dtos/request/reset-password.request.js +2 -0
- package/dist/application/dtos/request/verify-email.request.d.ts +3 -0
- package/dist/application/dtos/request/verify-email.request.js +2 -0
- package/dist/application/dtos/response/change-password.response.d.ts +7 -0
- package/dist/application/dtos/response/change-password.response.js +2 -0
- package/dist/application/dtos/response/index.d.ts +5 -0
- package/dist/application/dtos/response/index.js +5 -0
- package/dist/application/dtos/response/login.response.d.ts +2 -1
- package/dist/application/dtos/response/me.response.d.ts +11 -0
- package/dist/application/dtos/response/me.response.js +2 -0
- package/dist/application/dtos/response/refresh-token.response.d.ts +1 -0
- package/dist/application/dtos/response/register-user.response.d.ts +9 -0
- package/dist/application/dtos/response/request-password-reset.response.d.ts +15 -0
- package/dist/application/dtos/response/request-password-reset.response.js +2 -0
- package/dist/application/dtos/response/reset-password.response.d.ts +7 -0
- package/dist/application/dtos/response/reset-password.response.js +2 -0
- package/dist/application/dtos/response/verify-email.response.d.ts +4 -0
- package/dist/application/dtos/response/verify-email.response.js +2 -0
- package/dist/application/dtos/types/user-role.type.js +2 -0
- package/dist/application/facades/auth.facade.d.ts +33 -0
- package/dist/application/facades/auth.facade.js +60 -0
- package/dist/application/facades/create-auth-facade.d.ts +22 -0
- package/dist/application/facades/create-auth-facade.js +9 -0
- package/dist/application/facades/index.d.ts +2 -0
- package/dist/application/facades/index.js +18 -0
- package/dist/application/factories/auth-service.factory.d.ts +4 -4
- package/dist/application/factories/auth-service.factory.js +16 -4
- package/dist/application/factories/index.js +1 -0
- package/dist/application/types/auth-service-factory-options.type.d.ts +44 -0
- package/dist/application/use-cases/change-password.use-case.d.ts +21 -0
- package/dist/application/use-cases/change-password.use-case.js +49 -0
- package/dist/application/use-cases/index.d.ts +5 -0
- package/dist/application/use-cases/index.js +5 -0
- package/dist/application/use-cases/internal/index.d.ts +1 -0
- package/dist/application/use-cases/internal/index.js +17 -0
- package/dist/application/use-cases/internal/password-assertions.d.ts +13 -0
- package/dist/application/use-cases/internal/password-assertions.js +26 -0
- package/dist/application/use-cases/login-with-password.use-case.js +24 -29
- package/dist/application/use-cases/logout.use-case.js +14 -2
- package/dist/application/use-cases/me.use-case.d.ts +7 -0
- package/dist/application/use-cases/me.use-case.js +28 -0
- package/dist/application/use-cases/refresh-token.use-case.d.ts +16 -2
- package/dist/application/use-cases/refresh-token.use-case.js +33 -5
- package/dist/application/use-cases/register-user.use-case.d.ts +6 -1
- package/dist/application/use-cases/register-user.use-case.js +18 -7
- package/dist/application/use-cases/request-password-reset.use-case.d.ts +19 -0
- package/dist/application/use-cases/request-password-reset.use-case.js +43 -0
- package/dist/application/use-cases/reset-password.use-case.d.ts +20 -0
- package/dist/application/use-cases/reset-password.use-case.js +59 -0
- package/dist/application/use-cases/verify-email.use-case.d.ts +8 -0
- package/dist/application/use-cases/verify-email.use-case.js +30 -0
- package/dist/domain/entities/credential.entity.d.ts +36 -11
- package/dist/domain/entities/credential.entity.js +41 -11
- package/dist/domain/entities/user.entity.d.ts +32 -1
- package/dist/domain/entities/user.entity.js +54 -1
- package/dist/domain/errors/auth-error-code.d.ts +16 -0
- package/dist/domain/errors/auth-error-code.js +60 -0
- package/dist/domain/errors/auth.errors.d.ts +29 -8
- package/dist/domain/errors/auth.errors.js +59 -8
- package/dist/domain/errors/general.errors.d.ts +4 -0
- package/dist/domain/errors/general.errors.js +10 -0
- package/dist/domain/errors/identity.errors.d.ts +17 -12
- package/dist/domain/errors/identity.errors.js +29 -25
- package/dist/domain/errors/index.d.ts +5 -0
- package/dist/domain/errors/index.js +5 -0
- package/dist/domain/errors/jwt-payload.error.d.ts +4 -0
- package/dist/domain/errors/jwt-payload.error.js +10 -0
- package/dist/domain/errors/jwt.errors.d.ts +7 -0
- package/dist/domain/errors/jwt.errors.js +16 -0
- package/dist/domain/errors/password-reset.errors.d.ts +14 -0
- package/dist/domain/errors/password-reset.errors.js +29 -0
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.js +1 -0
- package/dist/domain/ports/auth/email-verification-token.port.d.ts +19 -0
- package/dist/domain/ports/auth/email-verification-token.port.js +2 -0
- package/dist/domain/ports/auth/index.d.ts +2 -0
- package/dist/domain/ports/auth/index.js +2 -0
- package/dist/domain/ports/auth/password-reset-token.port.d.ts +36 -0
- package/dist/domain/ports/auth/password-reset-token.port.js +2 -0
- package/dist/domain/ports/jwt/payload/jwt-payload.port.d.ts +25 -3
- package/dist/domain/ports/repository/credential.repository.d.ts +55 -2
- package/dist/domain/ports/token/token-session.port.d.ts +2 -0
- package/dist/domain/props/entities/credential.props.d.ts +9 -1
- package/dist/domain/props/entities/user.props.d.ts +1 -0
- package/dist/domain/props/jwt/generate-access-token.props.d.ts +3 -2
- package/dist/domain/props/jwt/generate-refresh-token.props.d.ts +3 -2
- package/dist/domain/props/jwt/jwt-user.d.ts +11 -2
- package/dist/domain/services/helpers/assert-jwt-structure.helper.d.ts +1 -0
- package/dist/domain/services/helpers/assert-jwt-structure.helper.js +13 -0
- package/dist/domain/services/helpers/index.d.ts +7 -0
- package/dist/domain/services/helpers/index.js +23 -0
- package/dist/domain/services/helpers/optional-audience.helper.d.ts +14 -0
- package/dist/domain/services/helpers/optional-audience.helper.js +49 -0
- package/dist/domain/services/helpers/optional-non-empty-string.helper.d.ts +1 -0
- package/dist/domain/services/helpers/optional-non-empty-string.helper.js +9 -0
- package/dist/domain/services/helpers/optional-record.helper.d.ts +1 -0
- package/dist/domain/services/helpers/optional-record.helper.js +15 -0
- package/dist/domain/services/helpers/optional-roles.helper.d.ts +3 -0
- package/dist/domain/services/helpers/optional-roles.helper.js +32 -0
- package/dist/domain/services/helpers/require-finite-number.helper.d.ts +1 -0
- package/dist/domain/services/helpers/require-finite-number.helper.js +12 -0
- package/dist/domain/services/helpers/require-non-empty-string.helper.d.ts +1 -0
- package/dist/domain/services/helpers/require-non-empty-string.helper.js +12 -0
- package/dist/domain/services/index.d.ts +1 -0
- package/dist/domain/services/index.js +1 -0
- package/dist/domain/services/normalize-jwt-payload.service.d.ts +19 -0
- package/dist/domain/services/normalize-jwt-payload.service.js +58 -0
- package/dist/domain/types/access-snapshot.type.d.ts +15 -0
- package/dist/domain/types/access-snapshot.type.js +2 -0
- package/dist/domain/types/index.d.ts +1 -0
- package/dist/domain/types/index.js +2 -0
- package/dist/in-memory/in-memory-credential.repository.d.ts +66 -3
- package/dist/in-memory/in-memory-credential.repository.js +174 -46
- package/dist/index.d.ts +18 -2
- package/dist/index.js +24 -9
- package/dist/infrastructure/index.d.ts +3 -0
- package/dist/infrastructure/index.js +18 -0
- package/dist/infrastructure/security/bcrypt-password-hasher.js +0 -1
- package/dist/infrastructure/services/token-session.service.d.ts +163 -8
- package/dist/infrastructure/services/token-session.service.js +290 -37
- package/dist/infrastructure/types/auth-service-container.d.ts +21 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/jwt-plugin/create-jwt-id.d.ts +6 -0
- package/dist/shared/jwt-plugin/create-jwt-id.js +30 -0
- package/dist/shared/jwt-plugin/index.d.ts +9 -0
- package/dist/shared/jwt-plugin/index.js +25 -0
- package/dist/shared/jwt-plugin/is-retryable-auth-code.d.ts +8 -0
- package/dist/shared/jwt-plugin/is-retryable-auth-code.js +15 -0
- package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.d.ts +14 -0
- package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.js +23 -0
- package/dist/shared/jwt-plugin/normalize-default-expires-in.d.ts +16 -0
- package/dist/shared/jwt-plugin/normalize-default-expires-in.js +36 -0
- package/dist/shared/jwt-plugin/read-custom-claims.d.ts +12 -0
- package/dist/shared/jwt-plugin/read-custom-claims.js +21 -0
- package/dist/shared/jwt-plugin/read-expires-in.d.ts +12 -0
- package/dist/shared/jwt-plugin/read-expires-in.js +20 -0
- package/dist/shared/jwt-plugin/read-session-id.d.ts +11 -0
- package/dist/shared/jwt-plugin/read-session-id.js +17 -0
- package/dist/shared/jwt-plugin/resolve-expires-in.d.ts +14 -0
- package/dist/shared/jwt-plugin/resolve-expires-in.js +31 -0
- package/dist/shared/jwt-plugin/to-date-from-unix-seconds.d.ts +7 -0
- package/dist/shared/jwt-plugin/to-date-from-unix-seconds.js +12 -0
- package/package.json +12 -11
- /package/dist/application/dtos/{type/user-role.type.js → request/change-password.request.js} +0 -0
- /package/dist/application/dtos/{type → types}/index.d.ts +0 -0
- /package/dist/application/dtos/{type → types}/index.js +0 -0
- /package/dist/application/dtos/{type → types}/user-role.type.d.ts +0 -0
|
@@ -16,3 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./password-hasher.port"), exports);
|
|
18
18
|
__exportStar(require("./password-policy.port"), exports);
|
|
19
|
+
__exportStar(require("./password-reset-token.port"), exports);
|
|
20
|
+
__exportStar(require("./email-verification-token.port"), exports);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Id } from "../../object-values";
|
|
2
|
+
/**
|
|
3
|
+
* Contrato para emitir/verificar/consumir tokens de reseteo de contraseña.
|
|
4
|
+
*
|
|
5
|
+
* - No usa JWT de access/refresh (separación de responsabilidades).
|
|
6
|
+
* - Debe soportar TTL y "single-use" (consumir token).
|
|
7
|
+
* - La implementación vive fuera del core (API REST: DB/Redis/etc).
|
|
8
|
+
*/
|
|
9
|
+
export interface IPasswordResetTokenPort {
|
|
10
|
+
/**
|
|
11
|
+
* Emite un token para permitir reset de password.
|
|
12
|
+
*
|
|
13
|
+
* @param userId Usuario destino del reset.
|
|
14
|
+
* @param ttlMs Tiempo de vida en milisegundos.
|
|
15
|
+
*/
|
|
16
|
+
issue(userId: Id, ttlMs: number): Promise<{
|
|
17
|
+
token: string;
|
|
18
|
+
expiresAt: Date;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Verifica token sin consumirlo.
|
|
22
|
+
* Útil para validaciones previas si la implementación lo requiere.
|
|
23
|
+
*/
|
|
24
|
+
verify(token: string): Promise<{
|
|
25
|
+
userId: Id;
|
|
26
|
+
expiresAt: Date;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Consume el token (single-use). Si ya fue usado o es inválido, debe fallar.
|
|
30
|
+
* Recomendación: consumo atómico (transacción) en implementaciones con DB/Redis.
|
|
31
|
+
*/
|
|
32
|
+
consume(token: string): Promise<{
|
|
33
|
+
userId: Id;
|
|
34
|
+
expiresAt: Date;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
@@ -1,12 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload canónico del core (@jmlq/auth).
|
|
3
|
+
*
|
|
4
|
+
* Importante:
|
|
5
|
+
* - NO depende de librerías (jose, jsonwebtoken, etc.)
|
|
6
|
+
* - El runtime del plugin entrega "claims" como unknown => el core normaliza/valida.
|
|
7
|
+
*/
|
|
1
8
|
export interface IJWTPayload {
|
|
9
|
+
/** Subject (user ID) */
|
|
2
10
|
sub: string;
|
|
3
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Roles embebidos en token (opcional).
|
|
13
|
+
* Nota: en escenarios más estrictos, el host puede NO incluir roles en el JWT.
|
|
14
|
+
*/
|
|
15
|
+
roles?: Array<{
|
|
4
16
|
role: string;
|
|
5
17
|
}>;
|
|
6
|
-
customClaims: Record<string, any>;
|
|
7
18
|
jti: string;
|
|
8
19
|
iss?: string;
|
|
9
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Audience puede venir como string o string[] (dependiendo de la lib).
|
|
22
|
+
* El core lo acepta y valida.
|
|
23
|
+
*/
|
|
24
|
+
aud?: string | string[];
|
|
10
25
|
iat: number;
|
|
11
26
|
exp: number;
|
|
27
|
+
/** claim de sesión (dispositivo) */
|
|
28
|
+
sid: string;
|
|
29
|
+
/**
|
|
30
|
+
* Custom claims del token (opcional).
|
|
31
|
+
* Evitar `any`: los consumers deben castear explícitamente si requieren shape.
|
|
32
|
+
*/
|
|
33
|
+
customClaims?: Record<string, unknown>;
|
|
12
34
|
}
|
|
@@ -1,10 +1,63 @@
|
|
|
1
1
|
import { Credential } from "../../entities";
|
|
2
2
|
import { Id } from "../../object-values";
|
|
3
|
+
/**
|
|
4
|
+
* Repositorio de credenciales (sesiones) por usuario/dispositivo.
|
|
5
|
+
*
|
|
6
|
+
* - Un usuario puede tener múltiples credenciales activas (una por sessionId).
|
|
7
|
+
* - Las operaciones deben permitir revocar por sessionId (logout dispositivo)
|
|
8
|
+
* y por userId (logout global).
|
|
9
|
+
*/
|
|
3
10
|
export interface ICredentialRepositoryPort {
|
|
11
|
+
/**
|
|
12
|
+
* Guarda una credencial.
|
|
13
|
+
* Recomendación: tratarlo como UPSERT por (userId, sessionId).
|
|
14
|
+
*/
|
|
4
15
|
save(credential: Credential): Promise<void>;
|
|
5
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Devuelve todas las sesiones activas del usuario.
|
|
18
|
+
* Antes era 1 credencial; ahora debe ser colección.
|
|
19
|
+
*/
|
|
20
|
+
findByUserId(userId: Id): Promise<Credential[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Busca una sesión específica por su identificador (dispositivo).
|
|
23
|
+
*/
|
|
24
|
+
findBySessionId(sessionId: Id): Promise<Credential | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Busca credencial por refresh token (para rotación).
|
|
27
|
+
* Debe ser único.
|
|
28
|
+
*/
|
|
6
29
|
findByRefreshToken(refreshToken: string): Promise<Credential | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Actualiza una credencial existente.
|
|
32
|
+
* (Opcional si `save` es UPSERT, pero se mantiene para compatibilidad).
|
|
33
|
+
*/
|
|
7
34
|
update(credential: Credential): Promise<void>;
|
|
8
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Elimina todas las sesiones del usuario (logout global).
|
|
37
|
+
* Reemplaza al anterior `delete(userId)` para evitar ambigüedad.
|
|
38
|
+
*/
|
|
39
|
+
deleteByUserId(userId: Id): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Elimina una sesión por su sessionId (logout por dispositivo).
|
|
42
|
+
*/
|
|
43
|
+
deleteBySessionId(sessionId: Id): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Elimina una sesión por refresh token (logout basado en refresh).
|
|
46
|
+
*/
|
|
9
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>;
|
|
10
63
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { Id } from "../../object-values";
|
|
1
2
|
import { Credential, User } from "../../entities";
|
|
2
3
|
export interface ITokenSessionPort {
|
|
3
4
|
createSession(user: User): Promise<Credential>;
|
|
4
5
|
refreshSession(refreshToken: string): Promise<Credential>;
|
|
5
6
|
validateSession(accessToken: string): Promise<User | null>;
|
|
6
7
|
revokeSession(refreshToken: string): Promise<void>;
|
|
8
|
+
revokeSessionById(sessionId: Id): Promise<void>;
|
|
7
9
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { Id } from "../../object-values";
|
|
2
2
|
export interface ICredentialProps {
|
|
3
|
+
sessionId: Id;
|
|
3
4
|
userId: Id;
|
|
4
5
|
accessToken: string;
|
|
5
|
-
|
|
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;
|
|
6
14
|
expiresAt: Date;
|
|
7
15
|
createdAt: Date;
|
|
8
16
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export interface JwtUser {
|
|
2
|
+
/**
|
|
3
|
+
* Identificador único del usuario (claim `sub`).
|
|
4
|
+
*/
|
|
2
5
|
id: string;
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Opcional: algunas apps pueden incluirlo, pero no es requerido por el core.
|
|
8
|
+
*/
|
|
9
|
+
email?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Opcional: JWT delgado (RBAC en BDD).
|
|
12
|
+
*/
|
|
13
|
+
roles?: {
|
|
5
14
|
role: string;
|
|
6
15
|
}[];
|
|
7
16
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertJwtStructure(token: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertJwtStructure = assertJwtStructure;
|
|
4
|
+
const errors_1 = require("../../../domain/errors");
|
|
5
|
+
function assertJwtStructure(token) {
|
|
6
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
7
|
+
throw new errors_1.InvalidJwtEmptyError();
|
|
8
|
+
}
|
|
9
|
+
const parts = token.split(".");
|
|
10
|
+
if (parts.length !== 3) {
|
|
11
|
+
throw new errors_1.InvalidJwtMalformedError();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./optional-non-empty-string.helper";
|
|
2
|
+
export * from "./require-non-empty-string.helper";
|
|
3
|
+
export * from "./require-finite-number.helper";
|
|
4
|
+
export * from "./optional-audience.helper";
|
|
5
|
+
export * from "./optional-roles.helper";
|
|
6
|
+
export * from "./optional-record.helper";
|
|
7
|
+
export * from "./assert-jwt-structure.helper";
|
|
@@ -0,0 +1,23 @@
|
|
|
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("./optional-non-empty-string.helper"), exports);
|
|
18
|
+
__exportStar(require("./require-non-empty-string.helper"), exports);
|
|
19
|
+
__exportStar(require("./require-finite-number.helper"), exports);
|
|
20
|
+
__exportStar(require("./optional-audience.helper"), exports);
|
|
21
|
+
__exportStar(require("./optional-roles.helper"), exports);
|
|
22
|
+
__exportStar(require("./optional-record.helper"), exports);
|
|
23
|
+
__exportStar(require("./assert-jwt-structure.helper"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regla canónica de dominio para el claim estándar `aud` (audience).
|
|
3
|
+
*
|
|
4
|
+
* Propósito:
|
|
5
|
+
* - Aceptar `aud` como string o string[] (según librería/plugin).
|
|
6
|
+
* - Rechazar valores vacíos.
|
|
7
|
+
* - Si es array: normalizar de forma determinista (dedupe + sort),
|
|
8
|
+
* útil para tests y debugging.
|
|
9
|
+
*
|
|
10
|
+
* Importante:
|
|
11
|
+
* - Esta validación es del CORE (@jmlq/auth).
|
|
12
|
+
* - Los plugins JWT NO deben validar audience; solo entregan payload verificado.
|
|
13
|
+
*/
|
|
14
|
+
export declare function optionalAudience(value: unknown): string | string[] | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optionalAudience = optionalAudience;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
/**
|
|
6
|
+
* Regla canónica de dominio para el claim estándar `aud` (audience).
|
|
7
|
+
*
|
|
8
|
+
* Propósito:
|
|
9
|
+
* - Aceptar `aud` como string o string[] (según librería/plugin).
|
|
10
|
+
* - Rechazar valores vacíos.
|
|
11
|
+
* - Si es array: normalizar de forma determinista (dedupe + sort),
|
|
12
|
+
* útil para tests y debugging.
|
|
13
|
+
*
|
|
14
|
+
* Importante:
|
|
15
|
+
* - Esta validación es del CORE (@jmlq/auth).
|
|
16
|
+
* - Los plugins JWT NO deben validar audience; solo entregan payload verificado.
|
|
17
|
+
*/
|
|
18
|
+
function optionalAudience(value) {
|
|
19
|
+
// aud ausente => ok
|
|
20
|
+
if (value == null)
|
|
21
|
+
return undefined;
|
|
22
|
+
// aud como string
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
const v = value.trim();
|
|
25
|
+
if (!v) {
|
|
26
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.aud must not be empty", {
|
|
27
|
+
field: "aud",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
// aud como string[]
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
const items = value
|
|
35
|
+
.filter((x) => typeof x === "string")
|
|
36
|
+
.map((x) => x.trim())
|
|
37
|
+
.filter((x) => x.length > 0);
|
|
38
|
+
if (items.length === 0) {
|
|
39
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.aud must contain at least one non-empty string", { field: "aud" });
|
|
40
|
+
}
|
|
41
|
+
// Determinista: sin duplicados y ordenado
|
|
42
|
+
return Array.from(new Set(items)).sort((a, b) => a.localeCompare(b));
|
|
43
|
+
}
|
|
44
|
+
// Tipo inválido
|
|
45
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.aud must be a string or string[]", {
|
|
46
|
+
field: "aud",
|
|
47
|
+
receivedType: typeof value,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readNonEmptyString(value: unknown): string | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readNonEmptyString = readNonEmptyString;
|
|
4
|
+
function readNonEmptyString(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
const v = value.trim();
|
|
8
|
+
return v.length > 0 ? v : undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function optionalRecord(value: unknown, field: string): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optionalRecord = optionalRecord;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
function optionalRecord(value, field) {
|
|
6
|
+
if (value == null)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
9
|
+
throw new errors_1.InvalidJwtPayloadError(`JWT payload.${field} must be an object`, {
|
|
10
|
+
field,
|
|
11
|
+
receivedType: typeof value,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optionalRoles = optionalRoles;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
function optionalRoles(value) {
|
|
6
|
+
if (value == null)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!Array.isArray(value)) {
|
|
9
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.roles must be an array", {
|
|
10
|
+
field: "roles",
|
|
11
|
+
receivedType: typeof value,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const item of value) {
|
|
16
|
+
if (item == null || typeof item !== "object") {
|
|
17
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.roles items must be objects", {
|
|
18
|
+
field: "roles",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const obj = item;
|
|
22
|
+
const role = obj.role;
|
|
23
|
+
if (typeof role !== "string" || !role.trim()) {
|
|
24
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload.roles[].role must be a non-empty string", {
|
|
25
|
+
field: "roles.role",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
out.push({ role: role.trim() });
|
|
29
|
+
}
|
|
30
|
+
// Si vino array vacío, lo retornamos como [] (válido).
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function requireFiniteNumber(value: unknown, field: string): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireFiniteNumber = requireFiniteNumber;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
function requireFiniteNumber(value, field) {
|
|
6
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
7
|
+
throw new errors_1.InvalidJwtPayloadError(`JWT payload.${field} must be a finite number`, {
|
|
8
|
+
field,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function requireNonEmptyString(value: unknown, field: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireNonEmptyString = requireNonEmptyString;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
function requireNonEmptyString(value, field) {
|
|
6
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
7
|
+
throw new errors_1.InvalidJwtPayloadError(`JWT payload.${field} must be a non-empty string`, {
|
|
8
|
+
field,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
@@ -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("./password-policy.service"), exports);
|
|
18
|
+
__exportStar(require("./normalize-jwt-payload.service"), exports);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IJWTPayload } from "../ports";
|
|
2
|
+
/**
|
|
3
|
+
* Domain Service
|
|
4
|
+
* --------------
|
|
5
|
+
* Normaliza y valida un payload JWT según las reglas del dominio.
|
|
6
|
+
*
|
|
7
|
+
* - Entrada: unknown (claims verificados por infraestructura/plugin)
|
|
8
|
+
* - Salida: IJWTPayload tipado y confiable
|
|
9
|
+
*
|
|
10
|
+
* Importante:
|
|
11
|
+
* - NO verifica firma
|
|
12
|
+
* - NO parsea JWT
|
|
13
|
+
* - Define únicamente reglas de dominio
|
|
14
|
+
*
|
|
15
|
+
* Contrato:
|
|
16
|
+
* - `aud` se valida exclusivamente aquí vía `optionalAudience()`.
|
|
17
|
+
* - Cualquier error de `aud` debe provenir de `InvalidJwtPayloadError`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeJwtPayload(input: unknown): IJWTPayload;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeJwtPayload = normalizeJwtPayload;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
/**
|
|
7
|
+
* Domain Service
|
|
8
|
+
* --------------
|
|
9
|
+
* Normaliza y valida un payload JWT según las reglas del dominio.
|
|
10
|
+
*
|
|
11
|
+
* - Entrada: unknown (claims verificados por infraestructura/plugin)
|
|
12
|
+
* - Salida: IJWTPayload tipado y confiable
|
|
13
|
+
*
|
|
14
|
+
* Importante:
|
|
15
|
+
* - NO verifica firma
|
|
16
|
+
* - NO parsea JWT
|
|
17
|
+
* - Define únicamente reglas de dominio
|
|
18
|
+
*
|
|
19
|
+
* Contrato:
|
|
20
|
+
* - `aud` se valida exclusivamente aquí vía `optionalAudience()`.
|
|
21
|
+
* - Cualquier error de `aud` debe provenir de `InvalidJwtPayloadError`.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeJwtPayload(input) {
|
|
24
|
+
if (input == null || typeof input !== "object") {
|
|
25
|
+
throw new errors_1.InvalidJwtPayloadError("JWT payload must be an object", {
|
|
26
|
+
receivedType: typeof input,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const obj = input;
|
|
30
|
+
// Required
|
|
31
|
+
const sub = (0, helpers_1.requireNonEmptyString)(obj.sub, "sub");
|
|
32
|
+
const sid = (0, helpers_1.requireNonEmptyString)(obj.sid, "sid");
|
|
33
|
+
const jti = (0, helpers_1.requireNonEmptyString)(obj.jti, "jti");
|
|
34
|
+
const iat = (0, helpers_1.requireFiniteNumber)(obj.iat, "iat");
|
|
35
|
+
const exp = (0, helpers_1.requireFiniteNumber)(obj.exp, "exp");
|
|
36
|
+
// Optional
|
|
37
|
+
const iss = (0, helpers_1.readNonEmptyString)(obj.iss);
|
|
38
|
+
/**
|
|
39
|
+
* Canonical audience rule (core):
|
|
40
|
+
* - string | string[] | undefined
|
|
41
|
+
* - empty string or empty array => InvalidJwtPayloadError
|
|
42
|
+
* - array => dedupe + sort
|
|
43
|
+
*/
|
|
44
|
+
const aud = (0, helpers_1.optionalAudience)(obj.aud);
|
|
45
|
+
const roles = (0, helpers_1.optionalRoles)(obj.roles);
|
|
46
|
+
const customClaims = (0, helpers_1.optionalRecord)(obj.customClaims, "customClaims");
|
|
47
|
+
return {
|
|
48
|
+
sub,
|
|
49
|
+
sid,
|
|
50
|
+
jti,
|
|
51
|
+
iat,
|
|
52
|
+
exp,
|
|
53
|
+
...(iss ? { iss } : {}),
|
|
54
|
+
...(aud ? { aud } : {}),
|
|
55
|
+
...(roles ? { roles } : {}),
|
|
56
|
+
...(customClaims ? { customClaims } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot de acceso “en este instante”.
|
|
3
|
+
*
|
|
4
|
+
* “Snapshot” porque representa una fotografía de roles/permisos efectivos
|
|
5
|
+
* en un momento dado (por ejemplo: al momento de emitir el token o responder /me).
|
|
6
|
+
*
|
|
7
|
+
* Importante:
|
|
8
|
+
* - No implica que sea la "fuente de verdad" para siempre.
|
|
9
|
+
* - La fuente de verdad real es la BDD (y se revalida por middleware en el host).
|
|
10
|
+
*/
|
|
11
|
+
export type AccessSnapshot = Readonly<{
|
|
12
|
+
userId: string;
|
|
13
|
+
roles: readonly string[];
|
|
14
|
+
permissions: readonly string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { AccessSnapshot } from "./access-snapshot.type";
|
|
@@ -1,12 +1,75 @@
|
|
|
1
1
|
import { Credential, ICredentialRepositoryPort, Id } from "../domain";
|
|
2
|
+
/**
|
|
3
|
+
* Implementación en memoria del repositorio de credenciales.
|
|
4
|
+
*
|
|
5
|
+
* Soporta múltiples sesiones por usuario (multi-dispositivo) usando `sessionId`.
|
|
6
|
+
*/
|
|
2
7
|
export declare class InMemoryCredentialRepository implements ICredentialRepositoryPort {
|
|
3
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Fuente de verdad: credenciales indexadas por sessionId.
|
|
10
|
+
*/
|
|
11
|
+
private credentialsBySessionId;
|
|
12
|
+
/**
|
|
13
|
+
* Índice: userId -> sessionIds (para obtener todas las sesiones del usuario).
|
|
14
|
+
*/
|
|
15
|
+
private sessionsByUserId;
|
|
16
|
+
/**
|
|
17
|
+
* Índice: refreshToken -> sessionId (para refresh rotation).
|
|
18
|
+
*
|
|
19
|
+
* Regla:
|
|
20
|
+
* - Si un refresh token rota, el anterior se “consume” (se elimina del índice).
|
|
21
|
+
* - Esto permite emular la semántica single-use de una rotación atómica en DB.
|
|
22
|
+
*/
|
|
4
23
|
private refreshTokenIndex;
|
|
24
|
+
/**
|
|
25
|
+
* Rotación atómica de refresh token (single-use).
|
|
26
|
+
*
|
|
27
|
+
* Retorna:
|
|
28
|
+
* - true: si `currentRefreshToken` existía y coincidía con la credencial actual de la sesión,
|
|
29
|
+
* y se reemplazó por `nextCredential`.
|
|
30
|
+
* - false: si el refresh token no existe, ya fue consumido, o no coincide con la credencial actual.
|
|
31
|
+
*
|
|
32
|
+
* Invariante:
|
|
33
|
+
* - `nextCredential.sessionId` debe ser el mismo sessionId de la credencial actual.
|
|
34
|
+
*/
|
|
35
|
+
rotateByRefreshToken(currentRefreshToken: string, nextCredential: Credential): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Guarda (upsert) una credencial por (userId, sessionId).
|
|
38
|
+
* - Si existía una credencial para ese sessionId, limpia el índice de refreshToken viejo.
|
|
39
|
+
* - Actualiza el índice userId -> sessionIds.
|
|
40
|
+
*/
|
|
5
41
|
save(credential: Credential): Promise<void>;
|
|
6
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Devuelve todas las credenciales (sesiones) del usuario.
|
|
44
|
+
*/
|
|
45
|
+
findByUserId(userId: Id): Promise<Credential[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Busca una credencial por sessionId.
|
|
48
|
+
*/
|
|
49
|
+
findBySessionId(sessionId: Id): Promise<Credential | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Busca una credencial por refresh token.
|
|
52
|
+
*/
|
|
7
53
|
findByRefreshToken(refreshToken: string): Promise<Credential | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Actualiza una credencial existente.
|
|
56
|
+
* (Mantiene compatibilidad con el port; internamente equivale a save + validación).
|
|
57
|
+
*/
|
|
8
58
|
update(credential: Credential): Promise<void>;
|
|
9
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Elimina todas las sesiones del usuario (logout global).
|
|
61
|
+
*/
|
|
62
|
+
deleteByUserId(userId: Id): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Elimina una sesión por sessionId (logout por dispositivo).
|
|
65
|
+
*/
|
|
66
|
+
deleteBySessionId(sessionId: Id): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Elimina una sesión por refresh token.
|
|
69
|
+
*/
|
|
10
70
|
deleteByRefreshToken(refreshToken: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Método auxiliar para tests.
|
|
73
|
+
*/
|
|
11
74
|
clear(): void;
|
|
12
75
|
}
|