hpower-backend-core
v1.2.14
Published
Librería con modulos basados en NestJS para la integración con backends de la organización que lo requieran
Maintainers
Readme
HP Core Library
Librería NestJS que proporciona servicios de AWS (S3, Secrets Manager, SQS), autenticación JWT con JWKS, y sistema de caché con soporte para memoria y Redis.
📦 Instalación
Requisitos Previos
- Node.js v16+ y npm v8+
Instalación
npm install hpower-backend-coreInstalar Dependencias Peer
npm install @nestjs/common @nestjs/core reflect-metadata express cookie-parser
npm install --save-dev @types/express @types/cookie-parserDependencias Opcionales
Para usar el Sistema de Caché
Si deseas usar el sistema de caché integrado, instala:
npm install @nestjs/cache-manager cache-managerPara usar Redis como Store de Caché
Si además deseas usar Redis (recomendado para producción):
npm install @keyv/redis keyv redisPara Validaciones de Configuración (Recomendado)
Para validar automáticamente la configuración del caché y prevenir errores:
npm install class-validator class-transformerCon estas dependencias instaladas, la librería validará automáticamente:
- Que el puerto de Redis esté entre 1-65535
- Que el número de base de datos de Redis esté entre 0-15
- Que TTL y max sean números positivos
- Y más validaciones específicas por tipo de store
Nota: La librería funcionará sin problemas sin estas dependencias. Si no las instalas y configuras caché, verás advertencias en los logs indicando las dependencias faltantes.
🚀 Uso en tu Proyecto
1. Configurar cookie-parser (Requerido)
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// IMPORTANTE: Configurar cookie-parser para que AuthGuard pueda leer cookies
app.use(cookieParser());
await app.listen(3000);
}
bootstrap();2. Importar el Módulo
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HPCoreModule, AwsJwksLoader } from 'hpower-backend-core';
@Module({
imports: [
ConfigModule.forRoot(),
// Opción A: Configuración directa
HPCoreModule.register({
aws: {
region: 'us-east-1',
s3: {
bucket: 'my-bucket',
},
secretsManager: {},
},
auth: {
jwt: {
loader: new AwsJwksLoader({
jwksUrl: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXX/.well-known/jwks.json',
issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXX',
audience: 'my-client-id',
}),
},
},
}),
// Opción B: Configuración asíncrona con ConfigService
HPCoreModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
aws: {
region: config.get('AWS_REGION', 'us-east-1'),
s3: {
bucket: config.get('AWS_S3_BUCKET'),
},
secretsManager: {},
},
auth: {
jwt: {
loader: new AwsJwksLoader({
jwksUrl: config.get('JWKS_URL'),
issuer: config.get('JWT_ISSUER'),
audience: config.get('JWT_AUDIENCE'),
}),
},
},
// Configuración de caché (OPCIONAL - por defecto usa memoria)
cache: {
ttl: 3600000, // 1 hora en ms
max: 100, // máximo de items en caché
store: 'memory', // 'memory' o 'redis'
},
// Ejemplo con Redis:
// cache: {
// store: 'redis',
// redis: {
// host: config.get('REDIS_HOST', 'localhost'),
// port: parseInt(config.get('REDIS_PORT', '6379')),
// password: config.get('REDIS_PASSWORD'),
// db: 0,
// },
// },
}),
}),
],
})
export class AppModule {}2. Usar los Servicios
S3Service
import { Injectable } from '@nestjs/common';
import { S3Service } from 'hpower-backend-core';
@Injectable()
export class FileService {
constructor(private readonly s3: S3Service) {}
async uploadFile(file: Express.Multer.File) {
const key = `uploads/${Date.now()}-${file.originalname}`;
const url = await this.s3.uploadFile(key, file.buffer, file.mimetype);
return { url, key };
}
async downloadFile(key: string) {
return this.s3.getFile(key);
}
async deleteFile(key: string) {
return this.s3.deleteFile(key);
}
getPublicUrl(key: string) {
return this.s3.getFileUrl(key);
}
}SecretManagerService
import { Injectable } from '@nestjs/common';
import { SecretManagerService } from 'hpower-backend-core';
@Injectable()
export class ConfigurationService {
constructor(private readonly secrets: SecretManagerService) {}
async getDatabaseCredentials() {
const username = await this.secrets.getSecretValue(
'db-credentials',
'username',
);
const password = await this.secrets.getSecretValue(
'db-credentials',
'password',
);
return { username, password };
}
async getApiKeys() {
return this.secrets.getSecretJson<{ apiKey: string }>('api-keys');
}
}JwtService
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from 'hpower-backend-core';
@Injectable()
export class AuthenticationService {
constructor(private readonly jwtService: JwtService) {}
async validateRequest(authHeader: string) {
const token = this.jwtService.extractTokenFromHeader(authHeader);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = await this.jwtService.verifyAsync({ token });
return payload;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}AuthGuard Híbrido con Renovación Automática
La librería incluye un AuthGuard híbrido con renovación automática de tokens. Soporta:
- Extracción de tokens desde cookies o header Authorization
- Renovación automática de access tokens expirados (no bloqueante)
- Migración paulatina - funciona con o sin cache
- Refresh tokens server-side - almacenados en Redis/memoria
🎯 Comportamiento Híbrido
El AuthGuard tiene 2 modos de operación según la configuración:
1. Sin cache → Solo valida (lanza 401 si expiró) 2. Con cache + authServiceUrl → Llama al endpoint de Auth MS para renovar
IMPORTANTE: Para renovación automática, todos los microservicios (incluyendo Auth MS) deben configurar authServiceUrl.
📝 Configuración
import { Module } from '@nestjs/common';
import { HPCoreModule } from 'hpower-backend-core';
@Module({
imports: [
HPCoreModule.register({
auth: {
jwt: {
loader: {
secretOrPrivateKey: process.env.JWT_SECRET,
sign: {
expiresIn: '15m',
},
},
},
// OPCIONAL: URL del Auth MS para renovación centralizada
authServiceUrl: process.env.AUTH_SERVICE_URL, // 'http://auth-service:3000'
},
cache: {
auth: {
type: 'redis',
config: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
ttl: 7 * 24 * 60 * 60 * 1000, // 7 días para refresh tokens
},
},
},
}),
],
})
export class AppModule {}🚀 Uso Básico
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'hpower-backend-core';
@Controller('products')
export class ProductsController {
@Get()
@UseGuards(AuthGuard)
getAll() {
// Si el token expiró y hay cache, se renueva automáticamente
// El frontend recibe el nuevo token en header "renew-token"
return { message: 'This is a protected resource' };
}
}🔄 Renovación Automática
Cuando el access token expira:
- AuthGuard detecta expiración
- Con cache + authServiceUrl: Llama a
POST /auth/refresh-tokendel Auth MS - Con cache sin authServiceUrl: Lanza 401 (requiere authServiceUrl)
- Sin cache: Lanza 401 → Frontend maneja retry
- Si se renueva: Continúa con la petición (no bloqueante)
- Agrega headers de respuesta:
renew-token: Nuevo access tokenx-jwt-id: JWT ID (solo en HTTPS en producción)
📋 Configuración por Microservicio
Auth MS (con renovación automática):
HPCoreModule.register({
auth: {
jwt: {
loader: {
secretOrPrivateKey: process.env.JWT_SECRET,
sign: {
expiresIn: '15m',
},
},
},
// Auth MS se llama a sí mismo para renovar tokens
authServiceUrl: process.env.AUTH_SERVICE_URL || 'http://localhost:3000',
},
cache: {
auth: {
type: 'redis',
config: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
ttl: 7 * 24 * 60 * 60 * 1000, // 7 días para refresh tokens
},
},
},
});Products MS (con renovación automática):
HPCoreModule.register({
auth: {
jwt: {
loader: {
secretOrPrivateKey: process.env.JWT_SECRET,
},
},
authServiceUrl: 'http://auth-service:3000', // ← Llama a Auth MS
},
cache: {
auth: {
type: 'redis',
config: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
},
},
},
});Orders MS (sin cache - migración paulatina):
HPCoreModule.register({
auth: {
jwt: {
loader: {
secretOrPrivateKey: process.env.JWT_SECRET,
},
},
},
// Sin cache → Lanza 401 si expira → Frontend retry
});🔑 Auth MS - Endpoint de Refresh
import { Controller, Post, Body, UnauthorizedException, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { CACHE_AUTH, JwtService } from 'hpower-backend-core';
@Controller('auth')
export class AuthController {
constructor(
private readonly jwtService: JwtService,
@Inject(CACHE_AUTH) private readonly cacheManager: Cache,
) {}
@Post('refresh-token')
async refreshToken(@Body() body: { jwtId: string }) {
// Buscar refresh token en cache
const refreshToken = await this.cacheManager.get<string>(
`refresh:${body.jwtId}`,
);
if (!refreshToken) {
throw new UnauthorizedException('refresh_token_not_found');
}
// Validar refresh token
const payload = await this.jwtService.verifyAsync({ token: refreshToken });
// Generar nuevo access token
const newAccessToken = this.jwtService.sign({ payload });
return { accessToken: newAccessToken };
}
@Post('login')
async login(@Body() credentials: any, @Res() response: Response) {
// ... validar credenciales
const jwtId = uuidv4();
// Generar access token
const accessToken = this.jwtService.sign({
payload: { ...userData, jwtId },
});
// Generar refresh token
const refreshToken = this.jwtService.sign({
payload: { ...userData, jwtId },
expiresIn: '7d',
});
// Guardar refresh token en cache
await this.cacheManager.set(
`refresh:${jwtId}`,
refreshToken,
7 * 24 * 60 * 60 * 1000,
);
// Enviar jwtId en header (solo HTTPS en producción)
response.setHeader('x-jwt-id', jwtId);
return response.json({ accessToken, user: userData });
}
}💻 Frontend - Interceptor para Headers de Renovación
// axios interceptor
import axios from 'axios';
axios.interceptors.response.use(
response => {
// Si viene header renew-token, actualizar
const renewToken = response.headers['renew-token'];
if (renewToken) {
localStorage.setItem('accessToken', renewToken);
console.log('Token renovado automáticamente');
}
// Opcional: guardar jwtId
const jwtId = response.headers['x-jwt-id'];
if (jwtId) {
localStorage.setItem('jwtId', jwtId);
}
return response;
},
async error => {
const originalRequest = error.config;
// Si recibe 401 y no es retry
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Opción 1: Usar jwtId del header del error
const jwtId = error.response.headers['x-jwt-id'];
// Opción 2: Decodificar token expirado (si no hay header)
// const jwtId = jwt_decode(localStorage.getItem('accessToken')).jwtId;
// Llamar a Auth MS para renovar
const { data } = await axios.post('/auth/refresh-token', { jwtId });
// Actualizar token
localStorage.setItem('accessToken', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
// Retry request original
return axios(originalRequest);
} catch (refreshError) {
// Refresh falló, redirigir a login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);🔒 Seguridad
- jwtId solo se expone en HTTPS: En producción, el header
x-jwt-idsolo se envía si la conexión es HTTPS - Refresh tokens server-side: Nunca salen del servidor, almacenados en cache
- Desarrollo flexible: En desarrollo siempre permite
x-jwt-idpara facilitar testing - Headers de renovación:
renew-tokense envía siempre que hay renovación exitosa
📊 Logs Automáticos
Con cache + authServiceUrl (renovación automática):
[AuthGuard] AuthGuard: 2 extractores configurados, CON cache, renovación vía endpoint: http://auth-service:3000/auth/refresh-token
[AuthGuard] AuthGuard: Token expirado
[AuthGuard] AuthGuard: Llamando a http://auth-service:3000/auth/refresh-token
[AuthGuard] AuthGuard: Token renovado exitosamente
[AuthGuard] Petición exitosa con token renovadoCon cache sin authServiceUrl:
[AuthGuard] AuthGuard: 2 extractores configurados, CON cache, sin renovación automática (debe configurar authServiceUrl)
[AuthGuard] AuthGuard: Token expirado
[AuthGuard] AuthGuard: Sin authServiceUrl configurado, no se puede renovar
[AuthGuard] Error: token_expiredSin cache:
[AuthGuard] AuthGuard: 2 extractores configurados, SIN cache (solo valida)
[AuthGuard] Error: token_expiredSistema de Caché
La librería incluye un sistema de caché integrado que soporta almacenamiento en memoria (por defecto) o Redis.
Uso básico del caché:
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class ProductosService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async obtenerProducto(id: string) {
const cacheKey = `producto:${id}`;
// Intentar obtener del caché
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
console.log('✅ Producto obtenido del caché');
return cached;
}
// Si no existe en caché, buscar en BD
console.log('🔍 Buscando producto en la BD...');
const producto = await this.buscarEnBaseDeDatos(id);
// Guardar en caché por 1 hora (3600000 ms)
await this.cacheManager.set(cacheKey, producto, 3600000);
return producto;
}
async actualizarProducto(id: string, data: any) {
const producto = await this.actualizarEnBaseDeDatos(id, data);
// Invalidar caché después de actualizar
await this.cacheManager.del(`producto:${id}`);
return producto;
}
async limpiarCacheTodosLosProductos() {
// Limpiar todo el caché
await this.cacheManager.reset();
}
}Uso del caché de autenticación (CACHE_AUTH):
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_AUTH } from 'hpower-backend-core';
import { Cache } from 'cache-manager';
@Injectable()
export class AuthSessionService {
constructor(@Inject(CACHE_AUTH) private authCache: Cache) {}
async guardarSesion(sessionId: string, userData: any) {
const cacheKey = `session:${sessionId}`;
// Guardar sesión por 24 horas
await this.authCache.set(cacheKey, userData, 86400000);
}
async obtenerSesion(sessionId: string) {
const cacheKey = `session:${sessionId}`;
return await this.authCache.get(cacheKey);
}
async eliminarSesion(sessionId: string) {
const cacheKey = `session:${sessionId}`;
await this.authCache.del(cacheKey);
}
async limpiarSesionesExpiradas() {
// Limpiar todas las sesiones
await this.authCache.reset();
}
}Métodos disponibles:
// Obtener valor del caché
const value = await this.cacheManager.get<T>('key');
// Guardar valor (con TTL opcional en ms)
await this.cacheManager.set('key', valor, 60000); // 1 minuto
// Eliminar valor específico
await this.cacheManager.del('key');
// Limpiar todo el caché
await this.cacheManager.reset();Ejemplo en un controlador:
import { Controller, Get, Post, Param, Body, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Controller('usuarios')
export class UsuariosController {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private usuariosService: UsuariosService,
) {}
@Get(':id')
async obtenerUsuario(@Param('id') id: string) {
const cacheKey = `usuario:${id}`;
// Buscar en caché primero
let usuario = await this.cacheManager.get(cacheKey);
if (!usuario) {
// Si no está en caché, buscar en BD
usuario = await this.usuariosService.findOne(id);
// Cachear por 10 minutos
await this.cacheManager.set(cacheKey, usuario, 600000);
}
return usuario;
}
@Post(':id')
async actualizarUsuario(@Param('id') id: string, @Body() data: any) {
const usuario = await this.usuariosService.update(id, data);
// Invalidar caché al actualizar
await this.cacheManager.del(`usuario:${id}`);
return usuario;
}
}Configuración del caché:
// Sin configuración - usa memoria por defecto con TTL de 1 hora
HPCoreModule.registerAsync({
useFactory: () => ({
auth: { /* ... */ },
// cache no configurado = memoria por defecto
}),
});
// Caché en memoria con configuración personalizada
HPCoreModule.registerAsync({
useFactory: () => ({
auth: { /* ... */ },
cache: {
ttl: 1800000, // 30 minutos
max: 200, // máximo 200 items
store: 'memory',
},
}),
});
// Caché con Redis
HPCoreModule.registerAsync({
useFactory: () => ({
auth: { /* ... */ },
cache: {
store: 'redis',
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: 0,
},
},
}),
});
// Nueva estructura con store dedicado para autenticación (RECOMENDADO)
HPCoreModule.registerAsync({
useFactory: () => ({
auth: { /* ... */ },
cache: {
// Configuración por defecto (CACHE_MANAGER)
ttl: 3600000,
max: 100,
store: 'memory',
// Store dedicado para sesiones de autenticación (CACHE_AUTH)
auth: {
type: 'redis',
config: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: 0,
ttl: 86400000, // 24 horas
},
onInit: async (cache) => {
console.log('✅ Cache de autenticación inicializado');
},
onError: async (error) => {
console.error('❌ Error en cache de autenticación:', error);
},
},
// Stores adicionales opcionales
custom: {
products: {
type: 'memory',
config: { ttl: 1800000, max: 500 },
},
},
},
}),
});⚠️ Importante: Si no configuras cache.auth, verás una advertencia en los logs recomendando configurarlo para gestión de sesiones de autenticación. El sistema usará la configuración por defecto como fallback.
Tipado Discriminado
TypeScript proporcionará autocompletado específico según el tipo de store seleccionado:
cache: {
auth: {
type: 'memory', // Al seleccionar 'memory'
config: {
ttl: 86400000, // ✅ Solo propiedades de MemoryCacheConfig
max: 100,
// host: 'localhost' ❌ Error: no existe en MemoryCacheConfig
}
}
}
// Para Redis
cache: {
auth: {
type: 'redis', // Al seleccionar 'redis'
config: {
host: 'localhost', // ✅ Propiedades de RedisCacheConfig
port: 6379,
password: 'secret',
db: 0,
ttl: 86400000,
max: 100,
}
}
}
// Para custom
cache: {
auth: {
type: 'custom', // Al seleccionar 'custom'
config: {
// ✅ Cualquier propiedad es válida
customProperty: 'value',
anyOtherProperty: 123,
}
}
}Validaciones Automáticas
Si instalas class-validator y class-transformer, la configuración se validará automáticamente:
npm install class-validator class-transformerValidaciones implementadas:
Para Memory Cache:
ttl: Debe ser un número entero positivomax: Debe ser un número entero positivo
Para Redis Cache:
host: Debe ser una cadena de textoport: Debe ser un número entre 1 y 65535password: Debe ser una cadena de textodb: Debe ser un número entre 0 y 15 (Redis soporta 16 bases de datos)ttl: Debe ser un número entero positivomax: Debe ser un número entero positivo
Ejemplo de error de validación:
cache: {
auth: {
type: 'redis',
config: {
port: 99999, // ❌ Error: Port debe ser menor o igual a 65535
db: 20, // ❌ Error: DB debe ser menor o igual a 15
}
}
}Los errores se mostrarán en los logs al iniciar la aplicación:
[CacheConfigValidator] ⚠️ Errores de validación en configuración de caché (redis):
- port: Port debe ser menor o igual a 65535
- db: DB debe ser menor o igual a 15
[HPCoreModule] ⚠️ Error al inicializar CACHE_AUTH: Configuración de caché inválida🎨 Decoradores
@AppId()
Extrae y valida el UUID del header appid.
import { Controller, Post, Body } from '@nestjs/common';
import { AppId } from 'hpower-backend-core';
@Controller('resources')
export class ResourcesController {
@Post()
create(@AppId() appId: string, @Body() data: any) {
// appId ya está validado como UUID
return { appId, data };
}
}Validaciones automáticas:
- Header
appidobligatorio - Valor debe ser UUID válido
- Lanza
BadRequestExceptionsi falla
📝 Variables de Entorno (.env)
# AWS
AWS_REGION=us-east-1
AWS_S3_BUCKET=my-bucket-name
# JWT/Auth
JWT_SECRET=your-super-secret-jwt-key
JWKS_URL=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXX/.well-known/jwks.json
JWT_ISSUER=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXX
JWT_AUDIENCE=your-client-id
# Auth Service (opcional - solo para microservicios que NO son Auth)
# URL del servicio de autenticación para renovación de tokens
AUTH_SERVICE_URL=http://auth-service:3000
# Redis (opcional - solo si usas store: 'redis')
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password🔄 Actualizar la Librería
Cuando hagas cambios en backend-core:
# 1. En el proyecto backend-core
npm run build
# 2. Si usas npm link, los cambios se reflejan automáticamente
# 3. Si usas path local, reinstala:
cd /ruta/a/tu/proyecto
npm install🛠️ Desarrollo
Desarrollo Local con Proyectos de Ejemplo
Si estás desarrollando la librería y usas proyectos locales que la referencian con "hpower-backend-core": "file:...", es importante evitar conflictos de tipos de TypeScript.
Problema: Cuando hp-core y el proyecto que lo usa tienen diferentes versiones de @nestjs/common, TypeScript genera errores de tipo:
error TS2322: Type 'DynamicModule' is not assignable to type 'DynamicModule'
SyntaxError: await is only valid in async functionsSolución Recomendada:
El proyecto que usa hpower-backend-core debe usar la misma versión de NestJS que la librería:
// En tu proyecto que usa hpower-backend-core
{
"dependencies": {
"hpower-backend-core": "file:../path/to/hp-core",
"@nestjs/common": "^10.0.0", // ⚠️ Misma versión que hp-core
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
// Si usas caché:
"@nestjs/cache-manager": "^2.3.0",
"cache-manager": "^5.7.6"
}
}Alternativa: Usa npm link para mejor resolución de dependencias:
# En hp-core
npm link
# En tu proyecto
npm link hpower-backend-coreCompilación
# Compilar
npm run build
# Compilar en modo watch (recompila automáticamente)
npm run build:watch📚 Módulos Disponibles
- HPCoreModule - Módulo principal que orquesta AWS, Auth y Cache
- AWSModule - Módulo interno de AWS (S3, Secrets Manager, SQS)
- AuthModule - Módulo interno de autenticación JWT
- CacheModule - Sistema de caché con soporte para memoria y Redis (integrado en HPCoreModule)
🔧 Servicios Exportados
- S3Service - Operaciones con AWS S3
- SecretManagerService - Operaciones con AWS Secrets Manager
- SQSService - Operaciones con AWS SQS
- JwtService - Verificación de tokens JWT
- MemoryCacheService - Implementación de caché en memoria
- RedisCacheService - Implementación de caché con Redis
- AuthGuard - Guard de autenticación JWT
📖 Interfaces y Constantes Exportadas
Interfaces
- HPCoreModuleOptions - Configuración del módulo principal
- AWSModuleOptions - Configuración de AWS
- AuthModuleOptions - Configuración de autenticación
- CacheModuleOptions - Configuración del sistema de caché
- CacheStoreDefinition - Definición de un store de caché nombrado (type, config, onInit, onError)
- ICacheService - Interfaz del servicio de caché
- TokenExtractor - Función extractora de tokens
- JwtServiceInterface - Interfaz del servicio JWT
- JwtLoaderClass - Clase base para loaders JWT
Constantes
- CACHE_MANAGER - Token de inyección para el caché principal
- CACHE_AUTH - Token de inyección para el caché de autenticación/sesiones
- AUTH_JWT_SERVICE - Token de inyección para el servicio JWT
- AUTH_TOKEN_EXTRACTORS - Token de inyección para extractores de token
🎯 Extractores de Token
La librería exporta extractores de token que puedes usar:
- extractAuthTokensFromCookies - Extrae tokens desde cookies
- extractTokenFromAuthorizationHeader - Extrae token desde header Authorization
🔐 Loaders JWT
- AwsJwksLoader - Loader para JWKS desde AWS Cognito
- BaseLoader - Clase base para crear loaders personalizados
🚀 Standalone (Lambda & Serverless)
La librería incluye utilidades standalone para usar en contextos sin NestJS (como AWS Lambda):
- JwtStandalone - Cliente JWT standalone para verificación de tokens
- lambdaAuthorizerHelper - Helper para crear Lambda Authorizers
- LambdaAuthorizerEvent - Types para eventos de Lambda Authorizer
💾 Sistema de Caché - Características
Stores Soportados
Memoria (Memory) - Por defecto
- No requiere instalación adicional
- Ideal para desarrollo y aplicaciones pequeñas
- Datos se pierden al reiniciar la aplicación
- Cada instancia de la app tiene su propio caché
Redis
- Requiere
@keyv/redis,keyvyredis - Ideal para producción y aplicaciones distribuidas
- Persistencia de datos (según configuración de Redis)
- Caché compartido entre múltiples instancias
- Requiere
Múltiples Stores Nombrados
La librería soporta configurar múltiples stores de caché con nombres personalizados:
- CACHE_MANAGER - Store principal por defecto (inyección con
@Inject(CACHE_MANAGER)) - CACHE_AUTH - Store dedicado para sesiones de autenticación (inyección con
@Inject(CACHE_AUTH)) - Puedes agregar más stores personalizados según tus necesidades
Cada store puede usar un tipo diferente (memoria o Redis) y tener su propia configuración de TTL, max items, etc.
Configuración por Defecto
Si no configuras el módulo de caché, se usa:
- Store: Memoria
- TTL: 3600000 ms (1 hora)
- Max items: 100
Mejores Prácticas
Usa claves descriptivas:
const cacheKey = `usuario:${id}:perfil`; const cacheKey = `productos:categoria:${categoriaId}:pagina:${page}`;Invalida el caché cuando sea necesario:
// Después de actualizar await this.cacheManager.del(`usuario:${id}`); // Después de eliminar await this.cacheManager.del(`producto:${id}`);Usa TTL apropiados según el tipo de dato:
// Datos que cambian poco: 1 hora o más await this.cacheManager.set('config', data, 3600000); // Datos que cambian frecuentemente: 5-10 minutos await this.cacheManager.set('dashboard', data, 300000); // Datos en tiempo real: 30 segundos - 1 minuto await this.cacheManager.set('stock', data, 30000);Manejo de errores:
try { const cached = await this.cacheManager.get(key); if (cached) return cached; } catch (error) { // Si falla el caché, continuar con BD console.warn('Cache error:', error); } return await this.buscarEnBaseDeDatos(id);
Cuándo usar Memoria vs Redis
Usa Memoria cuando:
- Estés en desarrollo local
- Tu aplicación tiene una sola instancia
- Los datos cacheados son pequeños
- No necesitas persistencia
Usa Redis cuando:
- Estés en producción
- Tu aplicación tiene múltiples instancias (horizontal scaling)
- Necesitas compartir caché entre servicios
- Requieres persistencia del caché
- Manejas grandes volúmenes de datos
🚨 Sistema de Excepciones con Logging Automático
La librería incluye un sistema completo de excepciones con logging automático para HTTP y gRPC.
Características
- ✅ Logging automático: Cada excepción se registra automáticamente
- ✅ HTTP y gRPC: Soporte completo para ambos protocolos
- ✅ Metadata flexible: Pasa cualquier dato contextual que necesites
- ✅ Logger eficiente: Usa Winston (no console.log)
- ✅ Configurable: Cambia el logger por AWS CloudWatch, Grafana, etc.
- ✅ Niveles de severidad: LOW, MEDIUM, HIGH, CRITICAL
Instalación (Opcional)
Para usar el sistema de logging, instala Winston:
npm install winstonPara excepciones gRPC, instala:
npm install @nestjs/microservices @grpc/grpc-jsUso Básico
Excepciones HTTP
import {
BadRequestException,
UnauthorizedException,
NotFoundException,
InternalServerErrorException,
} from 'hpower-backend-core';
@Controller('users')
export class UsersController {
@Post()
create(@Body() data: any) {
if (!data.email) {
// Se registra automáticamente en los logs
throw new BadRequestException('Email is required');
}
// Con metadata adicional
throw new BadRequestException('Invalid email format', {
email: data.email,
userId: req.user?.id,
appId: req.headers.appid,
validationErrors: ['Invalid domain', 'Missing @'],
});
}
@Get(':id')
findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException('User not found', {
userId: id,
searchedAt: new Date().toISOString(),
});
}
return user;
}
}Excepciones gRPC
import {
InvalidArgumentException,
UnauthenticatedException,
GrpcNotFoundException,
} from 'hpower-backend-core';
@Controller()
export class AuthGrpcController {
@GrpcMethod('AuthService', 'ValidateToken')
validateToken(data: { token: string }) {
if (!data.token) {
throw new InvalidArgumentException('Token is required', {
service: 'AuthService',
method: 'ValidateToken',
});
}
const decoded = this.verify(data.token);
if (!decoded) {
throw new UnauthenticatedException('Invalid token', {
reason: 'JWT_VERIFICATION_FAILED',
service: 'AuthService',
});
}
return decoded;
}
}Excepciones Disponibles
HTTP (20+)
| Excepción | Código | Severidad | Uso |
|-----------|--------|-----------|-----|
| BadRequestException | 400 | MEDIUM | Datos inválidos |
| UnauthorizedException | 401 | HIGH | No autenticado |
| PaymentRequiredException | 402 | MEDIUM | Pago requerido |
| ForbiddenException | 403 | HIGH | Sin permisos |
| NotFoundException | 404 | MEDIUM | Recurso no existe |
| MethodNotAllowedException | 405 | MEDIUM | Método HTTP no permitido |
| NotAcceptableException | 406 | MEDIUM | Accept header inválido |
| RequestTimeoutException | 408 | MEDIUM | Timeout |
| ConflictException | 409 | MEDIUM | Conflicto (ej: duplicado) |
| GoneException | 410 | MEDIUM | Recurso eliminado |
| LengthRequiredException | 411 | MEDIUM | Content-Length faltante |
| PreconditionFailedException | 412 | MEDIUM | Precondición falló |
| PayloadTooLargeException | 413 | MEDIUM | Payload muy grande |
| UnsupportedMediaTypeException | 415 | MEDIUM | Content-Type inválido |
| UnprocessableEntityException | 422 | MEDIUM | Validación falló |
| TooManyRequestsException | 429 | MEDIUM | Rate limit excedido |
| InternalServerErrorException | 500 | CRITICAL | Error interno |
| NotImplementedException | 501 | HIGH | No implementado |
| BadGatewayException | 502 | HIGH | Gateway error |
| ServiceUnavailableException | 503 | CRITICAL | Servicio no disponible |
| GatewayTimeoutException | 504 | HIGH | Gateway timeout |
gRPC (13)
| Excepción | Código | Severidad | Uso |
|-----------|--------|-----------|-----|
| InvalidArgumentException | 3 | MEDIUM | Argumentos inválidos |
| DeadlineExceededException | 4 | HIGH | Timeout |
| GrpcNotFoundException | 5 | MEDIUM | No encontrado |
| AlreadyExistsException | 6 | MEDIUM | Ya existe |
| PermissionDeniedException | 7 | HIGH | Sin permisos |
| ResourceExhaustedException | 8 | HIGH | Recursos agotados |
| FailedPreconditionException | 9 | MEDIUM | Precondición falló |
| AbortedException | 10 | MEDIUM | Operación abortada |
| OutOfRangeException | 11 | MEDIUM | Fuera de rango |
| UnimplementedException | 12 | HIGH | No implementado |
| InternalException | 13 | CRITICAL | Error interno |
| UnavailableException | 14 | CRITICAL | No disponible |
| DataLossException | 15 | CRITICAL | Pérdida de datos |
| UnauthenticatedException | 16 | HIGH | No autenticado |
Configuración del Logger
Opción 1: Logger por defecto (Winston)
// Winston se usa automáticamente si está instalado
HPCoreModule.register({
auth: { /* ... */ },
});Opción 2: Logger personalizado
import { IExceptionLogger, ExceptionLogInfo } from 'hpower-backend-core';
// Crear logger personalizado para AWS CloudWatch
class CloudWatchLogger implements IExceptionLogger {
private cloudwatch: AWS.CloudWatchLogs;
constructor() {
this.cloudwatch = new AWS.CloudWatchLogs();
}
async logException(logInfo: ExceptionLogInfo): Promise<void> {
await this.cloudwatch.putLogEvents({
logGroupName: '/app/exceptions',
logStreamName: logInfo.protocol,
logEvents: [{
timestamp: new Date(logInfo.timestamp).getTime(),
message: JSON.stringify({
exception: logInfo.exceptionName,
message: logInfo.message,
code: logInfo.code,
severity: logInfo.severity,
...logInfo.metadata,
}),
}],
}).promise();
}
}
// Configurar en el módulo
HPCoreModule.register({
auth: { /* ... */ },
exceptionLogger: new CloudWatchLogger(),
});Opción 3: Logger para Grafana Loki
import axios from 'axios';
import { IExceptionLogger, ExceptionLogInfo } from 'hpower-backend-core';
class GrafanaLokiLogger implements IExceptionLogger {
private lokiUrl: string;
constructor(lokiUrl: string) {
this.lokiUrl = lokiUrl;
}
async logException(logInfo: ExceptionLogInfo): Promise<void> {
await axios.post(`${this.lokiUrl}/loki/api/v1/push`, {
streams: [{
stream: {
job: 'exceptions',
protocol: logInfo.protocol,
severity: logInfo.severity,
exception: logInfo.exceptionName,
},
values: [[
String(Date.now() * 1000000), // nanoseconds
JSON.stringify({
message: logInfo.message,
code: logInfo.code,
...logInfo.metadata,
}),
]],
}],
});
}
}
HPCoreModule.register({
auth: { /* ... */ },
exceptionLogger: new GrafanaLokiLogger('http://loki:3100'),
});Niveles de Severidad
El sistema asigna automáticamente niveles de severidad:
LOW (Informativo)
- Códigos 2xx, 3xx (éxito, redirección)
MEDIUM (Advertencia)
- 400, 404, 409, 422, etc.
- Errores de validación y cliente
HIGH (Error)
- 401, 403 (seguridad)
- 500s (errores de servidor)
CRITICAL (Crítico)
- 500, 503 (errores graves que requieren atención inmediata)
- DATA_LOSS, INTERNAL en gRPC
Ejemplo de Log
Cuando lanzas una excepción, se registra automáticamente:
throw new UnauthorizedException('Token expired', {
userId: '123',
appId: 'app-456',
traceId: 'trace-789',
tokenAge: '2h',
});Salida en Winston (consola):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ EXCEPTION LOGGED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Exception: UnauthorizedException
Message: Token expired
Code: 401 (HTTP)
Severity: HIGH
Time: 2025-12-22T10:30:45.123Z
Metadata:
userId: "123"
appId: "app-456"
traceId: "trace-789"
tokenAge: "2h"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Mejores Prácticas
- Siempre incluye contexto útil en metadata:
throw new BadRequestException('Invalid input', {
userId: req.user.id,
appId: req.headers.appid,
field: 'email',
value: req.body.email,
validationRule: 'email_format',
});- Usa el decorador @AppId() para capturar automáticamente:
@Post()
create(@AppId() appId: string, @Body() data: any) {
throw new BadRequestException('Invalid data', {
appId, // Automáticamente incluido
data,
});
}- Para errores críticos, incluye stack trace:
try {
await dangerousOperation();
} catch (error) {
throw new InternalServerErrorException('Operation failed', {
originalError: error.message,
stack: error.stack,
});
}🔍 Request Tracking (Trazabilidad de Peticiones)
Sistema de seguimiento automático de peticiones que permite rastrear el flujo completo de una petición a través de toda la aplicación, tanto para HTTP como gRPC.
Características
- ✅ TrackId automático: Genera o extrae un ID único para cada petición
- ✅ Soporte HTTP y gRPC: Interceptors para ambos protocolos
- ✅ AsyncLocalStorage: Contexto disponible en toda la cadena asíncrona sin pasar parámetros
- ✅ Logging automático: Todos los logs y excepciones incluyen el trackId automáticamente
- ✅ Propagación de headers: El trackId se propaga en respuestas HTTP y metadata gRPC
- ✅ Configuración flexible: Múltiples opciones de configuración
- ✅ Habilitado por defecto: No requiere configuración adicional para empezar a usarlo
Uso Básico
El tracking está habilitado por defecto automáticamente:
import { HPCoreModule } from 'hpower-backend-core';
@Module({
imports: [
HPCoreModule.register({
auth: { /* ... */ },
// Request tracking está habilitado automáticamente
}),
],
})
export class AppModule {}Configuración personalizada:
HPCoreModule.register({
auth: { /* ... */ },
requestTracking: {
// Tipo de tracking: 'all' (default) | 'http' | 'grpc'
type: 'all',
// Nombre del header HTTP para el trackId
headerName: 'x-request-id', // default
// Generar trackId si no viene en el request
generateIfMissing: true, // default
// Incluir trackId en headers de respuesta
includeInResponse: true, // default
},
})Deshabilitar el tracking:
HPCoreModule.register({
auth: { /* ... */ },
requestTracking: {
enabled: false,
},
})Headers HTTP Soportados
El sistema acepta trackId desde múltiples headers (en orden de prioridad):
x-request-id(o el configurado enheaderName)x-correlation-idx-trace-id
Respuesta HTTP incluye:
x-request-id: El trackId usadox-correlation-id: Alias del trackId (estándar W3C)
Uso del RequestContextService
Inyección en servicios:
import { Injectable } from '@nestjs/common';
import { RequestContextService } from 'hpower-backend-core';
@Injectable()
export class MyService {
constructor(
private readonly contextService: RequestContextService,
) {}
async processData() {
// Obtener el trackId actual
const trackId = this.contextService.getTrackId();
console.log('Processing request:', trackId);
// Obtener todo el contexto
const context = this.contextService.getFullContext();
console.log('User:', context.userId);
console.log('Protocol:', context.protocol); // 'http' o 'grpc'
console.log('Path:', context.path);
}
}Métodos disponibles:
// Obtener el trackId actual
getTrackId(): string | undefined;
// Obtener el protocolo (HTTP o GRPC)
getProtocol(): ProtocolType | undefined;
// Obtener el userId del contexto
getUserId(): string | undefined;
// Obtener el appId del contexto
getAppId(): string | undefined;
// Obtener el contexto completo
getFullContext(): IRequestContext | undefined;
// Verificar si hay un contexto activo
hasContext(): boolean;
// Actualizar el contexto actual con datos adicionales
updateContext(updates: Partial<IRequestContext>): void;Logging Automático con TrackId
Todos los logs y excepciones incluyen automáticamente el trackId:
import { BadRequestException } from 'hpower-backend-core';
// La excepción automáticamente incluirá el trackId en el log
throw new BadRequestException('Datos inválidos', {
field: 'email',
});Formato de log:
❌ [trackId=123e4567-e89b-12d3-a456-426614174000] [2024-01-15T10:30:00.000Z] error: [BadRequestException] Datos inválidos | code=400 protocol=http | field="email"Propagación Service-to-Service
Cliente HTTP:
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { RequestContextService } from 'hpower-backend-core';
@Injectable()
export class ApiClient {
constructor(
private readonly http: HttpService,
private readonly contextService: RequestContextService,
) {}
async callExternalApi() {
const trackId = this.contextService.getTrackId();
return this.http.post(
'https://api.example.com/endpoint',
{ data: 'value' },
{
headers: {
'x-request-id': trackId, // Propagar el trackId
'x-source-service': 'my-service',
},
},
).toPromise();
}
}Cliente gRPC:
import { Injectable } from '@nestjs/common';
import { RequestContextService } from 'hpower-backend-core';
import { Metadata } from '@grpc/grpc-js';
@Injectable()
export class GrpcClient {
constructor(
private readonly contextService: RequestContextService,
) {}
async callGrpcService() {
const trackId = this.contextService.getTrackId();
const metadata = new Metadata();
metadata.set('x-request-id', trackId);
metadata.set('x-source-service', 'my-service');
// Usar metadata en la llamada gRPC
return this.grpcClient.method(request, metadata);
}
}Filtrar Logs por Petición
Con el trackId puedes filtrar todos los logs relacionados con una petición específica:
# Filtrar logs por trackId
grep "trackId=123e4567-e89b-12d3-a456-426614174000" application.log
# En sistemas de logging estructurado (JSON)
jq 'select(.trackId == "123e4567-e89b-12d3-a456-426614174000")' logs.jsonContexto de Petición
interface IRequestContext {
trackId: string; // ID único de la petición
protocol: ProtocolType; // 'http' | 'grpc'
method?: string; // 'GET', 'POST', etc. (solo HTTP)
path?: string; // Ruta o método gRPC
userId?: string; // ID del usuario autenticado
appId?: string; // ID de la aplicación
sourceService?: string; // Servicio origen (service-to-service)
timestamp: Date; // Inicio de la petición
metadata?: Record<string, any>; // Metadata adicional
}Integración con Herramientas de Observabilidad
El trackId se puede usar directamente con:
- Grafana Loki: Como label para filtrado de logs
- Elasticsearch: Como campo indexado para búsquedas
- Jaeger / Zipkin: Como Trace ID para tracing distribuido
- AWS CloudWatch: En log streams para correlación
Mejores Prácticas
- Siempre propaga el trackId en llamadas a servicios externos
- No regeneres el trackId en servicios downstream
- Usa el trackId en logs personalizados para correlación completa
- Incluye el trackId en respuestas de error para debugging
- Configura índices en bases de datos si guardas el trackId en logs
