npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-core

Instalar Dependencias Peer

npm install @nestjs/common @nestjs/core reflect-metadata express cookie-parser
npm install --save-dev @types/express @types/cookie-parser

Dependencias Opcionales

Para usar el Sistema de Caché

Si deseas usar el sistema de caché integrado, instala:

npm install @nestjs/cache-manager cache-manager

Para usar Redis como Store de Caché

Si además deseas usar Redis (recomendado para producción):

npm install @keyv/redis keyv redis

Para Validaciones de Configuración (Recomendado)

Para validar automáticamente la configuración del caché y prevenir errores:

npm install class-validator class-transformer

Con 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:

  1. Extracción de tokens desde cookies o header Authorization
  2. Renovación automática de access tokens expirados (no bloqueante)
  3. Migración paulatina - funciona con o sin cache
  4. 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:

  1. AuthGuard detecta expiración
  2. Con cache + authServiceUrl: Llama a POST /auth/refresh-token del Auth MS
  3. Con cache sin authServiceUrl: Lanza 401 (requiere authServiceUrl)
  4. Sin cache: Lanza 401 → Frontend maneja retry
  5. Si se renueva: Continúa con la petición (no bloqueante)
  6. Agrega headers de respuesta:
    • renew-token: Nuevo access token
    • x-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-id solo 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-id para facilitar testing
  • Headers de renovación: renew-token se 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 renovado

Con 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_expired

Sin cache:

[AuthGuard] AuthGuard: 2 extractores configurados, SIN cache (solo valida)
[AuthGuard] Error: token_expired

Sistema 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-transformer

Validaciones implementadas:

Para Memory Cache:

  • ttl: Debe ser un número entero positivo
  • max: Debe ser un número entero positivo

Para Redis Cache:

  • host: Debe ser una cadena de texto
  • port: Debe ser un número entre 1 y 65535
  • password: Debe ser una cadena de texto
  • db: Debe ser un número entre 0 y 15 (Redis soporta 16 bases de datos)
  • ttl: Debe ser un número entero positivo
  • max: 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 appid obligatorio
  • Valor debe ser UUID válido
  • Lanza BadRequestException si 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 functions

Solució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-core

Compilació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

  1. 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é
  2. Redis

    • Requiere @keyv/redis, keyv y redis
    • Ideal para producción y aplicaciones distribuidas
    • Persistencia de datos (según configuración de Redis)
    • Caché compartido entre múltiples instancias

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

  1. Usa claves descriptivas:

    const cacheKey = `usuario:${id}:perfil`;
    const cacheKey = `productos:categoria:${categoriaId}:pagina:${page}`;
  2. 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}`);
  3. 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);
  4. 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 winston

Para excepciones gRPC, instala:

npm install @nestjs/microservices @grpc/grpc-js

Uso 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

  1. 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',
});
  1. 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,
  });
}
  1. 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):

  1. x-request-id (o el configurado en headerName)
  2. x-correlation-id
  3. x-trace-id

Respuesta HTTP incluye:

  • x-request-id: El trackId usado
  • x-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.json

Contexto 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

  1. Siempre propaga el trackId en llamadas a servicios externos
  2. No regeneres el trackId en servicios downstream
  3. Usa el trackId en logs personalizados para correlación completa
  4. Incluye el trackId en respuestas de error para debugging
  5. Configura índices en bases de datos si guardas el trackId en logs