@jmlq/auth-plugin-jose 0.0.1-alpha.7 → 0.0.1-alpha.8

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,4 +1,4 @@
1
- import { type ITokenServicePort } from "@jmlq/auth";
1
+ import { JoseTokenService } from "../../infrastructure/services";
2
2
  import type { CreateAuthErrorFn } from "../../infrastructure/services/types";
3
3
  import { JoseTokenServiceOptions } from "../types";
4
4
  /**
@@ -11,4 +11,4 @@ import { JoseTokenServiceOptions } from "../types";
11
11
  */
12
12
  export declare function createJoseTokenService(options: JoseTokenServiceOptions, deps: {
13
13
  createAuthError: CreateAuthErrorFn;
14
- }): ITokenServicePort;
14
+ }): JoseTokenService;
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createJoseTokenService = createJoseTokenService;
4
- const auth_1 = require("@jmlq/auth");
5
4
  const services_1 = require("../../infrastructure/services");
6
5
  const internal_1 = require("./internal");
7
- const types_1 = require("../types");
8
6
  /**
9
7
  * Factory para construir un `ITokenServicePort` basado en `jose`.
10
8
  *
@@ -16,21 +14,13 @@ const types_1 = require("../types");
16
14
  function createJoseTokenService(options, deps) {
17
15
  (0, internal_1.assert)(options, "JoseTokenServiceOptions is required");
18
16
  (0, internal_1.assert)(deps?.createAuthError, "createAuthError dependency is required");
19
- const keyMaterial = (0, internal_1.validateKeyMaterial)(options.keyMaterial);
20
- // Normalización mínima local (string trim -> undefined) OK en application
21
- const issuer = (0, auth_1.readNonEmptyString)(options.issuer);
22
- // IMPORTANTE:
23
- // - clockSkewSeconds y defaultExpiresIn se delegan a infraestructura para
24
- // aplicar normalización estándar desde @jmlq/auth.
25
- const clockSkewSeconds = options.clockSkewSeconds;
26
- const defaultExpiresIn = options.defaultExpiresIn;
27
17
  return new services_1.JoseTokenService({
28
18
  options: {
29
- keyMaterial,
30
- issuer,
31
- clockSkewSeconds,
32
- defaultExpiresIn,
33
- getExpirationPolicy: types_1.DEFAULT_GET_EXPIRATION_POLICY,
19
+ keyMaterial: options.keyMaterial,
20
+ issuer: options.issuer,
21
+ audience: options.audience,
22
+ clockSkewSeconds: options.clockSkewSeconds,
23
+ defaultExpiresIn: options.defaultExpiresIn,
34
24
  },
35
25
  createAuthError: deps.createAuthError,
36
26
  });
@@ -1,5 +1,3 @@
1
1
  export * from "./jose-token-service-options.type";
2
2
  export * from "./jose-key-material.type";
3
3
  export * from "./default-expires-in.type";
4
- export * from "./get-expiration-policy.type";
5
- export { DEFAULT_GET_EXPIRATION_POLICY } from "./get-expiration-policy.type";
@@ -14,11 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.DEFAULT_GET_EXPIRATION_POLICY = void 0;
18
17
  __exportStar(require("./jose-token-service-options.type"), exports);
19
18
  __exportStar(require("./jose-key-material.type"), exports);
20
19
  __exportStar(require("./default-expires-in.type"), exports);
21
- __exportStar(require("./get-expiration-policy.type"), exports);
22
- // Constantes públicas
23
- var get_expiration_policy_type_1 = require("./get-expiration-policy.type");
24
- Object.defineProperty(exports, "DEFAULT_GET_EXPIRATION_POLICY", { enumerable: true, get: function () { return get_expiration_policy_type_1.DEFAULT_GET_EXPIRATION_POLICY; } });
@@ -1,6 +1,5 @@
1
1
  import type { JoseKeyMaterial } from "./jose-key-material.type";
2
2
  import type { DefaultExpiresIn } from "./default-expires-in.type";
3
- import type { GetExpirationPolicy } from "./get-expiration-policy.type";
4
3
  /**
5
4
  * Opciones públicas para construir el token service basado en jose.
6
5
  * Responsabilidad: configuración neutral, sin acoplar al consumidor a jose.
@@ -8,6 +7,14 @@ import type { GetExpirationPolicy } from "./get-expiration-policy.type";
8
7
  export interface JoseTokenServiceOptions {
9
8
  keyMaterial: JoseKeyMaterial;
10
9
  issuer?: string;
10
+ /**
11
+ * Audience opcional para firmar (claim `aud`).
12
+ * Puede ser string o string[].
13
+ *
14
+ * Nota: esto NO es validación de audience contra el token entrante.
15
+ * Es solo configuración para emisión (firmado).
16
+ */
17
+ audience?: string | string[];
11
18
  /**
12
19
  * Clock skew en segundos para validaciones temporales (iat/nbf/exp).
13
20
  */
@@ -16,9 +23,4 @@ export interface JoseTokenServiceOptions {
16
23
  * Expiraciones default si el token no define otra (por kind).
17
24
  */
18
25
  defaultExpiresIn?: DefaultExpiresIn;
19
- /**
20
- * Política acordada del plugin.
21
- * Se fuerza a "verify-first" desde la factory.
22
- */
23
- getExpirationPolicy?: GetExpirationPolicy;
24
26
  }
@@ -1,4 +1,4 @@
1
- import type { ITokenServicePort, IJWTPayload, IGenerateAccessTokenProps, IGenerateRefreshTokenProps } from "@jmlq/auth";
1
+ import { type IGenerateAccessTokenProps, type IGenerateRefreshTokenProps, type IJWTPayload, type ITokenServicePort } from "@jmlq/auth";
2
2
  import type { JoseTokenServiceOptions } from "../../application/types";
3
3
  import type { CreateAuthErrorFn } from "./types";
4
4
  /**
@@ -6,20 +6,24 @@ import type { CreateAuthErrorFn } from "./types";
6
6
  *
7
7
  * Responsabilidad única:
8
8
  * - Firmar/verificar JWT
9
- * - Delegar validación de payload al core (normalizeJwtPayload)
9
+ * - Delegar validación/normalización del payload al core (normalizeJwtPayload)
10
10
  *
11
11
  * Clean Architecture (decisión del proyecto):
12
- * - Aquí (infra) sí consumimos utilidades estándar desde @jmlq/auth,
13
- * para evitar duplicidad en cada plugin.
12
+ * - En infraestructura sí consumimos utilidades estándar desde @jmlq/auth
13
+ * (reglas canónicas), para evitar duplicidad en cada plugin.
14
14
  */
15
15
  export declare class JoseTokenService implements ITokenServicePort {
16
16
  private readonly options;
17
17
  private readonly createAuthError;
18
+ private readonly keyMaterial;
18
19
  /**
19
20
  * Config normalizada (estandarizada) para uso interno.
20
- * Se deriva 1 sola vez para evitar repetir lógica.
21
+ * Se deriva 1 sola vez para evitar repetir lógica y asegurar consistencia.
21
22
  */
22
23
  private readonly eff;
24
+ /**
25
+ * Cache lazy de llaves normalizadas (evita repetir trabajo por request).
26
+ */
23
27
  private keysPromise;
24
28
  constructor(cfg: {
25
29
  options: JoseTokenServiceOptions;
@@ -29,9 +33,37 @@ export declare class JoseTokenService implements ITokenServicePort {
29
33
  generateRefreshToken(props: IGenerateRefreshTokenProps): Promise<string>;
30
34
  verifyAccessToken(token: string): Promise<IJWTPayload>;
31
35
  verifyRefreshToken(token: string): Promise<IJWTPayload>;
36
+ /**
37
+ * Obtiene la fecha de expiración del token.
38
+ *
39
+ * Estrategia:
40
+ * - Primero intenta verificarlo como access.
41
+ * - Si falla con error “retryable”, intenta verificarlo como refresh.
42
+ * - Si está expirado pero se puede leer exp vía decode (sin verificación),
43
+ * retorna esa exp para fines informativos.
44
+ */
32
45
  getTokenExpiration(token: string): Promise<Date>;
46
+ /**
47
+ * Construye contexto mínimo (no sensible) para mapear errores.
48
+ *
49
+ * ¿Por qué existe?
50
+ * - Hace diagnósticos más fáciles (operación, tokenKind, alg, issuer).
51
+ * - Evita exponer token, claims completos o material criptográfico.
52
+ *
53
+ * Nota:
54
+ * - No incluimos audience en el contexto porque la audience en este plugin
55
+ * es SOLO para emisión y no participa en verificación (por decisión de diseño).
56
+ */
33
57
  private ctx;
58
+ /**
59
+ * Wrapper estándar:
60
+ * - Ejecuta fn
61
+ * - En caso de error, lo traduce a AuthDomainError usando el mapper del plugin
62
+ */
34
63
  private withAuthError;
64
+ /**
65
+ * Normaliza/carga llaves de forma lazy y cacheada.
66
+ */
35
67
  private getNormalizedKeys;
36
68
  private generateToken;
37
69
  private verifyToken;
@@ -1,29 +1,38 @@
1
1
  "use strict";
2
+ // src/infrastructure/services/jose-token.service.ts
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.JoseTokenService = void 0;
4
- const auth_1 = require("@jmlq/auth");
5
5
  const jose_1 = require("jose");
6
+ const auth_1 = require("@jmlq/auth");
7
+ const internal_1 = require("../../application/factories/internal");
6
8
  const mappers_1 = require("../mappers");
7
- const internal_1 = require("./internal");
9
+ const internal_2 = require("./internal");
8
10
  /**
9
11
  * Implementación de `ITokenServicePort` usando la librería `jose`.
10
12
  *
11
13
  * Responsabilidad única:
12
14
  * - Firmar/verificar JWT
13
- * - Delegar validación de payload al core (normalizeJwtPayload)
15
+ * - Delegar validación/normalización del payload al core (normalizeJwtPayload)
14
16
  *
15
17
  * Clean Architecture (decisión del proyecto):
16
- * - Aquí (infra) sí consumimos utilidades estándar desde @jmlq/auth,
17
- * para evitar duplicidad en cada plugin.
18
+ * - En infraestructura sí consumimos utilidades estándar desde @jmlq/auth
19
+ * (reglas canónicas), para evitar duplicidad en cada plugin.
18
20
  */
19
21
  class JoseTokenService {
20
22
  constructor(cfg) {
23
+ /**
24
+ * Cache lazy de llaves normalizadas (evita repetir trabajo por request).
25
+ */
21
26
  this.keysPromise = null;
22
27
  this.options = cfg.options;
23
28
  this.createAuthError = cfg.createAuthError;
24
- // Normalización estándar (core) aplicada en infraestructura
29
+ // Valida estructura de keyMaterial (alg / secret / keys etc.).
30
+ this.keyMaterial = (0, internal_1.validateKeyMaterial)(this.options.keyMaterial);
31
+ // Normalización canónica (core) aplicada en infraestructura.
32
+ // Nota: si el usuario envía strings con espacios, se estandariza aquí.
25
33
  this.eff = {
26
- issuer: cfg.options.issuer,
34
+ issuer: (0, auth_1.readNonEmptyString)(cfg.options.issuer), // " a " => "a", " " => undefined
35
+ audience: (0, auth_1.optionalAudience)(cfg.options.audience), // trim / dedupe+sort / errores canónicos
27
36
  clockSkewSeconds: (0, auth_1.normalizeClockSkewSeconds)(cfg.options.clockSkewSeconds),
28
37
  defaultExpiresIn: (0, auth_1.normalizeDefaultExpiresIn)(cfg.options.defaultExpiresIn),
29
38
  };
@@ -43,15 +52,16 @@ class JoseTokenService {
43
52
  async verifyRefreshToken(token) {
44
53
  return this.verifyToken("refresh", "verifyRefreshToken", token);
45
54
  }
55
+ /**
56
+ * Obtiene la fecha de expiración del token.
57
+ *
58
+ * Estrategia:
59
+ * - Primero intenta verificarlo como access.
60
+ * - Si falla con error “retryable”, intenta verificarlo como refresh.
61
+ * - Si está expirado pero se puede leer exp vía decode (sin verificación),
62
+ * retorna esa exp para fines informativos.
63
+ */
46
64
  async getTokenExpiration(token) {
47
- const policy = this.options.getExpirationPolicy ?? "verify-first";
48
- if (policy !== "verify-first") {
49
- throw this.createAuthError({
50
- code: "JWT_ERROR",
51
- message: "Unsupported expiration policy",
52
- meta: { operation: "getTokenExpiration", tokenKind: "unknown" },
53
- });
54
- }
55
65
  const firstTry = await this.tryGetExpirationByVerify("access", token);
56
66
  if (firstTry.ok)
57
67
  return firstTry.value;
@@ -65,14 +75,29 @@ class JoseTokenService {
65
75
  // ---------------------------------------------------------------------------
66
76
  // Internals - ctx + error wrapper
67
77
  // ---------------------------------------------------------------------------
78
+ /**
79
+ * Construye contexto mínimo (no sensible) para mapear errores.
80
+ *
81
+ * ¿Por qué existe?
82
+ * - Hace diagnósticos más fáciles (operación, tokenKind, alg, issuer).
83
+ * - Evita exponer token, claims completos o material criptográfico.
84
+ *
85
+ * Nota:
86
+ * - No incluimos audience en el contexto porque la audience en este plugin
87
+ * es SOLO para emisión y no participa en verificación (por decisión de diseño).
88
+ */
68
89
  ctx(operation, kind) {
69
- const alg = (0, internal_1.getAlgFromKeyMaterial)(this.options.keyMaterial);
70
- // FASE 3: sin audience en meta
71
- return (0, internal_1.buildJoseCtx)(operation, kind, {
90
+ const alg = (0, internal_2.getAlgFromKeyMaterial)(this.keyMaterial);
91
+ return (0, internal_2.buildJoseCtx)(operation, kind, {
72
92
  issuer: this.eff.issuer,
73
93
  alg,
74
94
  });
75
95
  }
96
+ /**
97
+ * Wrapper estándar:
98
+ * - Ejecuta fn
99
+ * - En caso de error, lo traduce a AuthDomainError usando el mapper del plugin
100
+ */
76
101
  async withAuthError(operation, kind, fn) {
77
102
  try {
78
103
  return await fn();
@@ -84,9 +109,12 @@ class JoseTokenService {
84
109
  // ---------------------------------------------------------------------------
85
110
  // Internals - keys
86
111
  // ---------------------------------------------------------------------------
112
+ /**
113
+ * Normaliza/carga llaves de forma lazy y cacheada.
114
+ */
87
115
  getNormalizedKeys() {
88
116
  if (!this.keysPromise) {
89
- this.keysPromise = (0, internal_1.normalizeKeyMaterial)(this.options.keyMaterial);
117
+ this.keysPromise = (0, internal_2.normalizeKeyMaterial)(this.keyMaterial);
90
118
  }
91
119
  return this.keysPromise;
92
120
  }
@@ -95,6 +123,7 @@ class JoseTokenService {
95
123
  // ---------------------------------------------------------------------------
96
124
  async generateToken(kind, operation, props) {
97
125
  return this.withAuthError(operation, kind, async () => {
126
+ // 1) Expiración: usar regla canónica del core (props > default).
98
127
  const expiresInFromProps = (0, auth_1.readExpiresIn)(props.expiresIn);
99
128
  const defaultExpiresIn = kind === "access"
100
129
  ? this.eff.defaultExpiresIn?.accessToken
@@ -104,17 +133,20 @@ class JoseTokenService {
104
133
  defaultExpiresIn,
105
134
  operation,
106
135
  });
136
+ // 2) Claims estándar
107
137
  const roles = props.user.roles ?? [];
108
- const customClaims = (0, auth_1.readCustomClaims)(props.customClaims) ??
109
- {};
138
+ const customClaims = (0, auth_1.readCustomClaims)(props.customClaims) ?? {};
139
+ // 3) Session Id (sid) debe ser un string válido.
110
140
  const sid = (0, auth_1.readSessionId)(props.sessionId);
111
141
  if (!sid) {
142
+ // Nota: aquí usamos createAuthError para mantener consistencia del core.
112
143
  throw this.createAuthError({
113
144
  code: "JWT_PAYLOAD_INVALID",
114
145
  message: "sessionId is required",
115
146
  meta: { operation, tokenKind: kind, field: "sessionId" },
116
147
  });
117
148
  }
149
+ // 4) Firmar
118
150
  const keys = await this.getNormalizedKeys();
119
151
  const jwt = new jose_1.SignJWT({ roles, customClaims, sid })
120
152
  .setProtectedHeader({ alg: keys.alg })
@@ -122,8 +154,13 @@ class JoseTokenService {
122
154
  .setJti((0, auth_1.createJwtId)())
123
155
  .setIssuedAt()
124
156
  .setExpirationTime(expiresIn);
157
+ // Issuer opcional (si viene configurado y es no-vacío)
125
158
  if (this.eff.issuer)
126
159
  jwt.setIssuer(this.eff.issuer);
160
+ // Audience opcional SOLO para emisión (claim `aud`).
161
+ // Importante: NO se usa en verificación para no volverlo requisito.
162
+ if (this.eff.audience)
163
+ jwt.setAudience(this.eff.audience);
127
164
  return keys.alg === "HS256"
128
165
  ? jwt.sign(keys.secret)
129
166
  : jwt.sign(keys.privateKey);
@@ -133,11 +170,21 @@ class JoseTokenService {
133
170
  return this.withAuthError(operation, kind, async () => {
134
171
  const keys = await this.getNormalizedKeys();
135
172
  const key = keys.alg === "HS256" ? keys.secret : keys.publicKey;
136
- // FASE 2/3: jwtVerify sin audience; clockSkew ya normalizado
173
+ /**
174
+ * Verificación:
175
+ * - issuer: se aplica si está configurado (si no, se omite)
176
+ * - clockTolerance: ya normalizado (clock skew seguro)
177
+ *
178
+ * Nota: NO pasamos `audience` a jwtVerify.
179
+ * - La audiencia en este plugin es SOLO para emisión.
180
+ * - Pasarla aquí convertiría audience en una restricción (“must match”)
181
+ * y rompería compatibilidad con tokens válidos sin `aud`.
182
+ */
137
183
  const result = await (0, jose_1.jwtVerify)(token, key, {
138
184
  issuer: this.eff.issuer,
139
185
  clockTolerance: this.eff.clockSkewSeconds,
140
186
  });
187
+ // Delegar a la regla canónica del core: valida + normaliza payload.
141
188
  return (0, auth_1.normalizeJwtPayload)(result.payload);
142
189
  });
143
190
  }
@@ -147,11 +194,15 @@ class JoseTokenService {
147
194
  return { ok: true, value: (0, auth_1.toDateFromUnixSeconds)(payload.exp) };
148
195
  }
149
196
  catch (err) {
197
+ // Si el error ya es un AuthDomainError, aplicamos reglas de retry/fallback.
150
198
  if (auth_1.AuthDomainError.isAuthError(err)) {
199
+ // Caso especial: si expiró, podemos leer exp por decode (sin verificar)
200
+ // para devolver la fecha de expiración con fines informativos.
151
201
  if (err.code === "TOKEN_EXPIRED") {
152
- const exp = (0, internal_1.tryReadExpByDecode)(token);
153
- if (exp !== null)
202
+ const exp = (0, internal_2.tryReadExpByDecode)(token);
203
+ if (exp !== null) {
154
204
  return { ok: true, value: (0, auth_1.toDateFromUnixSeconds)(exp) };
205
+ }
155
206
  }
156
207
  return {
157
208
  ok: false,
@@ -159,6 +210,7 @@ class JoseTokenService {
159
210
  error: err,
160
211
  };
161
212
  }
213
+ // Si es un error no estándar, lo envolvemos para mantener consistencia.
162
214
  const wrapped = (0, mappers_1.toAuthDomainError)(this.createAuthError, err, this.ctx("getTokenExpiration", kind));
163
215
  return { ok: false, retryable: false, error: wrapped };
164
216
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jmlq/auth-plugin-jose",
3
3
  "description": "Infrastructure plugin that integrates the jose library with @jmlq/auth, providing JWT token generation and verification following Clean Architecture principles.",
4
- "version": "0.0.1-alpha.7",
4
+ "version": "0.0.1-alpha.8",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  "author": "MLahuasi",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@jmlq/auth": "^0.0.1-alpha.27",
32
+ "@jmlq/auth": "^0.0.1-alpha.28",
33
33
  "jose": "^6.1.3"
34
34
  },
35
35
  "devDependencies": {
@@ -1,9 +0,0 @@
1
- /**
2
- * Política de expiración del plugin.
3
- * - "verify-first": verifica el token y si no tiene exp, aplica default.
4
- */
5
- export type GetExpirationPolicy = "verify-first";
6
- /**
7
- * Política por defecto del plugin (estándar acordado).
8
- */
9
- export declare const DEFAULT_GET_EXPIRATION_POLICY: GetExpirationPolicy;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_GET_EXPIRATION_POLICY = void 0;
4
- /**
5
- * Política por defecto del plugin (estándar acordado).
6
- */
7
- exports.DEFAULT_GET_EXPIRATION_POLICY = "verify-first";