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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@komarcalabs/auth-session

v0.1.7

Published

<a id="komarcalabsauth-session"></a>

Readme

@komarcalabs/auth-session

Una librería agnóstica y robusta para la gestión de autenticación y sesiones en aplicaciones front-end. Proporciona una API simple y flexible para manejar tokens de acceso, refresh tokens, y estados de sesión.

Características

  • 🔐 Gestión completa de tokens: Soporte para access tokens y refresh tokens
  • 💾 Múltiples estrategias de almacenamiento: localStorage, sessionStorage, memoria o cookies
  • 🔄 Renovación automática: Renovación automática de tokens antes de expirar
  • 📡 Sincronización multi-pestaña: Mantiene la sesión sincronizada automáticamente entre pestañas abiertas
  • Soporte No-JWT: Funciona con tokens JWT y tokens opacos
  • 🛡️ Sistema de eventos: Escucha cambios en el estado de autenticación
  • Validación de sesión: Verificación automática de expiración y validación personalizada
  • 🎯 TypeScript: Completamente tipado para una mejor experiencia de desarrollo
  • 🧪 Probado: Suite completa de tests unitarios
  • 🌐 Agnóstico: Funciona con cualquier framework o librería front-end

Instalación

npm install @komarcalabs/auth-session
# o
yarn add @komarcalabs/auth-session
# o
pnpm add @komarcalabs/auth-session

Documentación

La documentación completa está organizada en la carpeta docs:

📚 Índice de Documentación

  • Guía de Inicio Rápido

    • Instalación
    • Configuración básica
    • Primeros pasos
    • Flujo de autenticación con backend
    • Uso básico (login, logout, obtener token)
  • Ciclo de Vida de la Autenticación

    • Entendiendo getAccessToken (Síncrono vs Asíncrono)
    • Implementación con Interceptores (Axios/Fetch)
    • Renovación automática y expiración
  • Referencia de API

    • Métodos principales
    • Sistema de eventos
    • Tipos TypeScript
    • Interfaces y tipos exportados
  • Configuración

    • Opciones de configuración detalladas
    • Explicación de cada opción
    • Ejemplos de configuración
  • Token Decoder

    • ¿Qué es el token decoder?
    • ¿Cuándo necesitas un decoder personalizado?
    • Casos de uso específicos
    • Ejemplos prácticos
  • Estrategias de Almacenamiento

    • localStorage, sessionStorage, memory, cookie
    • Comparación y recomendaciones
  • Ejemplos por Escenarios

    • Apps Bancarias
    • Multi-Subdominio
    • SSR (Next.js)
    • Testing
  • Ejemplos de Uso

    • Servicio de autenticación completo
    • Integración con React
    • Interceptor de Fetch
    • Integración con Axios
    • Testing

Inicio Rápido

import { AuthSession } from '@komarcalabs/auth-session';

// Definir tu tipo de User personalizado
interface MyUser {
  id: string;
  role: 'admin' | 'user';
}

const authSession = new AuthSession<MyUser>({
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    return response.json();
  },
});

// Login
await authSession.login({
  accessToken: 'token-del-backend',
  refreshToken: 'refresh-token',
  expiresIn: 3600,
});

// Obtener token
const token = await authSession.getAccessToken();

Seguridad

  • Los tokens se almacenan de forma segura usando las APIs nativas del navegador
  • La librería no envía tokens automáticamente; tú controlas cuándo y cómo usarlos
  • Se recomienda usar HTTPS en producción
  • Considera implementar validación adicional en el servidor
  • Los refresh tokens deben ser manejados con cuidado y almacenados de forma segura

Licencia

MIT

Contribuir

Las contribuciones son bienvenidas. Por favor, abre un issue o pull request.


Guía de Inicio Rápido

Instalación

npm install @komarcalabs/auth-session
# o
yarn add @komarcalabs/auth-session
# o
pnpm add @komarcalabs/auth-session

Flujo de Autenticación con Backend

Sí, el JWT lo envía el backend. Esta librería es agnóstica y no hace peticiones HTTP por sí misma. Tu backend es quien:

  1. Genera los JWTs después de validar credenciales
  2. Envía los tokens en la respuesta del login
  3. Renueva los tokens cuando se solicita un refresh

El frontend solo:

  • Recibe los tokens del backend
  • Los almacena de forma segura
  • Los gestiona (renovación automática, validación, etc.)
  • Los usa para autenticar peticiones

Ejemplo Completo: Login con Backend

import { AuthSession } from '@komarcalabs/auth-session';

const authSession = new AuthSession({
  onRefreshToken: async (refreshToken) => {
    // El backend renueva el token
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Failed to refresh token');
    }

    // El backend devuelve los nuevos tokens
    const data = await response.json();
    return {
      accessToken: data.accessToken, // JWT del backend
      refreshToken: data.refreshToken, // Refresh token del backend
      expiresIn: data.expiresIn, // Tiempo de expiración
    };
  },
});

// Función de login que comunica con el backend
async function login(email: string, password: string) {
  // 1. Envías credenciales al backend
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });

  if (!response.ok) {
    throw new Error('Login failed');
  }

  // 2. El backend responde con los tokens (JWT)
  const data = await response.json();
  // Ejemplo de respuesta del backend:
  // {
  //   accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  //   refreshToken: "refresh_token_here",
  //   expiresIn: 3600,
  //   user: { id: "123", email: "[email protected]", name: "John" }
  // }

  // 3. Guardas los tokens en la librería
  await authSession.login(
    {
      accessToken: data.accessToken, // JWT del backend
      refreshToken: data.refreshToken,
      expiresIn: data.expiresIn,
    },
    data.user // Información del usuario (opcional)
  );
}

Respuesta Típica del Backend

El backend generalmente responde con algo como:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4",
  "expiresIn": 3600,
  "user": {
    "id": "123",
    "email": "[email protected]",
    "name": "John Doe",
    "roles": ["user"]
  }
}

Uso Básico

Configuración Inicial

import { AuthSession } from '@komarcalabs/auth-session';

const authSession = new AuthSession({
  // Opciones de configuración
  storageStrategy: 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
  storageKey: 'auth_tokens',
  userStorageKey: 'auth_user',
  defaultExpiresIn: 3600, // 1 hora en segundos
  refreshMargin: 300, // 5 minutos antes de expirar
  onRefreshToken: async (refreshToken) => {
    // Implementa tu lógica de renovación de token
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });
    const data = await response.json();
    return {
      accessToken: data.accessToken,
      refreshToken: data.refreshToken,
      expiresIn: data.expiresIn,
    };
  },
});

Login

// Después de un login exitoso
const tokens = {
  accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
  refreshToken: 'refresh_token_here', // Opcional: solo si tu backend lo envía
  expiresIn: 3600, // segundos
};

const user = {
  id: '123',
  email: '[email protected]',
  name: 'Juan Pérez',
  roles: ['user', 'admin'],
};

await authSession.login(tokens, user);

¿Qué pasa si el backend NO envía refresh token?

El refresh token es opcional. La librería funciona perfectamente sin él, pero con limitaciones:

Comportamiento sin refresh token:

  1. Login funciona normalmente: Puedes hacer login solo con accessToken

    await authSession.login({
      accessToken: 'token',
      expiresIn: 3600,
      // Sin refreshToken
    });
  2. getAccessToken() retorna el token mientras sea válido:

    • Si el token está válido: retorna el token
    • Si el token está expirado: retorna null (no puede renovarlo)
    • Si el token está cerca de expirar: retorna el token actual (no puede renovarlo preventivamente)
  3. refresh() lanza un error: Si intentas llamar refresh() sin refresh token, obtendrás un error:

    try {
      await authSession.refresh(); // Error: "No refresh token available"
    } catch (error) {
      console.error(error); // No se puede renovar sin refresh token
    }
  4. Cuando el token expira: La sesión se marca como 'expired' y necesitas hacer login nuevamente

Ejemplo: Manejo sin refresh token

const authSession = new AuthSession({
  // No necesitas onRefreshToken si no usas refresh tokens
});

// Login sin refresh token
await authSession.login({
  accessToken: 'token-del-backend',
  expiresIn: 3600,
});

// Usar el token mientras sea válido
const token = await authSession.getAccessToken();
if (token) {
  // Token válido, usarlo
  fetch('/api/protected', {
    headers: { Authorization: `Bearer ${token}` },
  });
} else {
  // Token expirado, redirigir a login
  window.location.href = '/login';
}

// Escuchar cuando expire
authSession.on('expired', () => {
  // Redirigir a login cuando expire
  window.location.href = '/login';
});

Recomendación:

  • Con refresh token: Sesiones más largas, renovación automática, mejor UX
  • Sin refresh token: Sesiones más cortas, el usuario debe hacer login nuevamente cuando expire

Obtener Token de Acceso

// Obtiene el token de acceso, renovándolo automáticamente si es necesario
const accessToken = await authSession.getAccessToken();

if (accessToken) {
  // Usa el token para hacer peticiones autenticadas
  const response = await fetch('/api/protected', {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
}

Verificar Autenticación

if (authSession.isAuthenticated()) {
  console.log('Usuario autenticado');
  const user = authSession.getUser();
  console.log('Usuario:', user);
}

Logout

await authSession.logout();

Renovación Manual de Token

try {
  const newTokens = await authSession.refresh();
  console.log('Tokens renovados:', newTokens);
} catch (error) {
  console.error('Error al renovar token:', error);
  // Manejar error, posiblemente redirigir a login
}

Referencia de API

Métodos Principales

login(tokens: AuthTokens, user?: User): Promise<void>

Inicia sesión guardando los tokens y opcionalmente la información del usuario.

await authSession.login(
  {
    accessToken: 'token',
    refreshToken: 'refresh',
    expiresIn: 3600,
  },
  {
    id: '123',
    email: '[email protected]',
  }
);

logout(): Promise<void>

Cierra la sesión y elimina todos los datos almacenados.

await authSession.logout();

getAccessToken(): Promise<string | null>

Obtiene el token de acceso actual. Si el token está expirado o cerca de expirar, intenta renovarlo automáticamente.

const token = await authSession.getAccessToken();

refresh(): Promise<AuthTokens>

Renueva el token de acceso usando el refresh token. Evita múltiples llamadas simultáneas.

const newTokens = await authSession.refresh();

isAuthenticated(): boolean

Verifica si hay una sesión activa y válida.

if (authSession.isAuthenticated()) {
  // Usuario autenticado
}

getUser(): User | null

Obtiene la información del usuario actual.

const user = authSession.getUser();

updateUser(user: Partial<User>): void

Actualiza parcialmente la información del usuario.

authSession.updateUser({ name: 'Nuevo Nombre' });

getSession(): SessionData | null

Obtiene la sesión completa (tokens + usuario).

const session = authSession.getSession();

validateSession(): Promise<boolean>

Valida la sesión actual, verificando expiración y ejecutando validadores personalizados.

const isValid = await authSession.validateSession();

getStatus(): SessionStatus

Obtiene el estado actual de la sesión: 'authenticated' | 'unauthenticated' | 'expired' | 'refreshing'.

const status = authSession.getStatus();

getRefreshToken(): string | null

Obtiene el refresh token actual.

const refreshToken = authSession.getRefreshToken();

Sistema de Eventos

Puedes escuchar eventos de autenticación para reaccionar a cambios:

// Escuchar evento de login
const unsubscribe = authSession.on('login', (event) => {
  console.log('Usuario inició sesión:', event.data);
});

// Escuchar evento de logout
authSession.on('logout', (event) => {
  console.log('Usuario cerró sesión');
});

// Escuchar evento de refresh
authSession.on('refresh', (event) => {
  console.log('Token renovado:', event.data);
});

// Escuchar evento de expiración
authSession.on('expired', (event) => {
  console.log('Sesión expirada');
  // Redirigir a login
});

// Escuchar errores
authSession.on('error', (event) => {
  console.error('Error de autenticación:', event.data);
});

// Desregistrar listener
unsubscribe();

Métodos de Eventos

on(eventType: AuthEventType, listener: AuthEventListener): () => void

Registra un listener para un tipo de evento. Retorna una función para desregistrar el listener.

off(eventType: AuthEventType, listener: AuthEventListener): void

Elimina un listener específico de un tipo de evento.

removeAllListeners(): void

Elimina todos los listeners registrados.

Tipos de eventos disponibles:

  • 'login': Se inició sesión
  • 'logout': Se cerró sesión
  • 'refresh': Se renovó el token
  • 'expired': La sesión expiró
  • 'error': Ocurrió un error

Tipos TypeScript

La librería exporta todos los tipos necesarios:

import type {
  AuthSessionOptions,
  AuthTokens,
  User,
  SessionData,
  SessionStatus,
  StorageStrategy,
  AuthEvent,
  AuthEventType,
  AuthEventListener,
} from '@komarcalabs/auth-session';

AuthTokens

interface AuthTokens {
  /** Token de acceso (access token) */
  accessToken: string;
  /** Token de renovación (refresh token) */
  refreshToken?: string;
  /** Tiempo de expiración del access token en segundos desde la emisión */
  expiresIn?: number;
  /** Timestamp de cuando se emitió el token */
  issuedAt?: number;
}

User

interface User {
  /** Identificador único del usuario */
  id: string | number;
  /** Email del usuario */
  email?: string;
  /** Nombre del usuario */
  name?: string;
  /** Roles del usuario */
  roles?: string[];
  /** Datos adicionales del usuario */
  [key: string]: unknown;
}

SessionData

interface SessionData {
  /** Tokens de autenticación */
  tokens: AuthTokens;
  /** Información del usuario */
  user?: User;
  /** Datos adicionales de la sesión */
  metadata?: Record<string, unknown>;
}

SessionStatus

type SessionStatus =
  | 'authenticated'
  | 'unauthenticated'
  | 'expired'
  | 'refreshing';

StorageStrategy

type StorageStrategy = 'localStorage' | 'sessionStorage' | 'memory';

Configuración

Opciones de Configuración

AuthSessionOptions

interface AuthSessionOptions {
  // Estrategia de almacenamiento
  storageStrategy?: 'localStorage' | 'sessionStorage' | 'memory';

  // Claves de almacenamiento
  storageKey?: string; // default: 'auth_tokens'
  userStorageKey?: string; // default: 'auth_user'

  // Configuración de expiración
  defaultExpiresIn?: number; // segundos, default: 3600
  refreshMargin?: number; // segundos antes de expirar, default: 300

  // Decodificador de token personalizado
  tokenDecoder?: (token: string) => {
    exp?: number;
    iat?: number;
    [key: string]: unknown;
  };

  // Validador de expiración personalizado
  isTokenExpired?: (
    token: string,
    issuedAt?: number,
    expiresIn?: number
  ) => boolean;

  // Callback para renovar token (opcional: solo necesario si usas refresh tokens)
  onRefreshToken?: (refreshToken: string) => Promise<AuthTokens>;

  // Validador de sesión personalizado
  onValidateSession?: (session: SessionData) => Promise<boolean>;
}

Opciones Detalladas

storageStrategy

Estrategia de almacenamiento para los tokens. Ver Estrategias de Almacenamiento para más detalles.

  • 'localStorage' (por defecto): Los tokens persisten entre sesiones
  • 'sessionStorage': Los tokens se eliminan al cerrar la pestaña
  • 'memory': Los tokens solo existen en memoria (útil para SSR o testing)

storageKey

Clave utilizada para almacenar los tokens en el storage. Por defecto: 'auth_tokens'.

userStorageKey

Clave utilizada para almacenar la información del usuario. Por defecto: 'auth_user'.

defaultExpiresIn

Tiempo de expiración por defecto en segundos si el backend no proporciona expiresIn.

  • Por defecto: 3600 (1 hora).
  • Infinito: Pasa null o 0 para que los tokens nunca expiren por tiempo (útil para estrategias donde solo importa la validez del token en el servidor).

refreshMargin

Tiempo en segundos antes de la expiración en el que se intentará renovar el token automáticamente. Por defecto: 300 (5 minutos).

Por ejemplo, si un token expira en 1 hora y refreshMargin es 5 minutos, el token se renovará automáticamente cuando queden 5 minutos o menos antes de expirar.

tokenDecoder

Función personalizada para decodificar el JWT y extraer información como exp (expiración) e iat (emitido en).

Por defecto, la librería incluye un decodificador básico que funciona con JWTs estándar. Solo necesitas proporcionar un decodificador personalizado si:

  • Tu backend usa un formato de token no estándar
  • Necesitas extraer información adicional del token
  • Quieres usar una librería específica como jwt-decode

Ver Token Decoder para más detalles y casos de uso.

isTokenExpired

Función personalizada para validar si un token está expirado. Por defecto, la librería usa la información del tokenDecoder o los parámetros issuedAt y expiresIn.

Solo necesitas proporcionar esta función si:

  • Tu lógica de expiración es diferente a la estándar
  • Necesitas validar expiración basada en otros criterios

onRefreshToken

Callback que se ejecuta cuando se necesita renovar el token. Opcional: solo necesario si usas refresh tokens.

Esta función debe:

  1. Recibir el refreshToken como parámetro
  2. Hacer una petición al backend para obtener nuevos tokens
  3. Retornar un objeto AuthTokens con los nuevos tokens
onRefreshToken: async (refreshToken: string) => {
  const response = await fetch('/api/auth/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken }),
  });

  if (!response.ok) {
    throw new Error('Failed to refresh token');
  }

  const data = await response.json();
  return {
    accessToken: data.accessToken,
    refreshToken: data.refreshToken,
    expiresIn: data.expiresIn,
  };
};

onValidateSession

Validador personalizado de sesión. Se ejecuta cuando llamas a validateSession().

onValidateSession: async (session: SessionData) => {
  // Verificar con el servidor que la sesión sigue siendo válida
  try {
    const response = await fetch('/api/auth/validate', {
      headers: {
        Authorization: `Bearer ${session.tokens.accessToken}`,
      },
    });
    return response.ok;
  } catch {
    return false;
  }
};

Sesiones Infinitas (Sin Expiración)

Si deseas que la sesión nunca expire automáticamente por parte del cliente (dejando la validación puramente al servidor o hasta que el usuario haga logout explícito), puedes configurar expiración infinita.

Opción A: Configuración Global

const authSession = new AuthSession({
  // ...
  defaultExpiresIn: null, // O usa 0
});

Opción B: Por Login

await authSession.login({
  accessToken: '...',
  expiresIn: null, // El token específico no expirará
});

Ejemplo de Configuración Completa

import { AuthSession } from '@komarcalabs/auth-session';
import jwtDecode from 'jwt-decode';

const authSession = new AuthSession({
  storageStrategy: 'localStorage',
  storageKey: 'my_app_auth_tokens',
  userStorageKey: 'my_app_user',
  defaultExpiresIn: 7200, // 2 horas
  refreshMargin: 600, // 10 minutos antes de expirar
  tokenDecoder: (token) => {
    return jwtDecode(token);
  },
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });
    const data = await response.json();
    return {
      accessToken: data.accessToken,
      refreshToken: data.refreshToken,
      expiresIn: data.expiresIn,
    };
  },
  onValidateSession: async (session) => {
    // Validación personalizada
    return true;
  },
});

Token Decoder

¿Qué es el Token Decoder?

El tokenDecoder es una función opcional que permite personalizar cómo se decodifica el JWT para extraer información como la fecha de expiración (exp) y la fecha de emisión (iat).

¿Cuándo necesitas un Token Decoder personalizado?

La librería incluye un decodificador básico por defecto que funciona con la mayoría de los JWTs estándar. Solo necesitas proporcionar un decodificador personalizado en los siguientes casos:

1. Usar una librería específica de decodificación

Si prefieres usar una librería como jwt-decode que puede manejar mejor casos edge o proporcionar mejor validación:

import jwtDecode from 'jwt-decode';

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    return jwtDecode(token);
  },
});

Ventajas:

  • Mejor manejo de errores
  • Validación más robusta
  • Soporte para diferentes formatos de JWT

2. Tokens con formato no estándar

Si tu backend genera tokens que no siguen el formato JWT estándar (aunque esto es raro):

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    // Tu lógica personalizada para decodificar
    try {
      // Ejemplo: token en formato base64 custom
      const decoded = JSON.parse(atob(token));
      return {
        exp: decoded.expiration,
        iat: decoded.issued_at,
      };
    } catch {
      return {};
    }
  },
});

3. Extraer información adicional del token

Si necesitas acceder a información adicional del payload del JWT (como roles, permisos, etc.):

import jwtDecode from 'jwt-decode';

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    const decoded = jwtDecode(token);
    // Ahora puedes acceder a decoded.roles, decoded.permissions, etc.
    return decoded;
  },
});

// Más tarde, puedes decodificar el token para obtener información adicional
const session = authSession.getSession();
if (session) {
  // Guarda una referencia al decoder si necesitas usarlo después
  const tokenDecoder = (token: string) => {
    return jwtDecode(token);
  };

  const decoded = tokenDecoder(session.tokens.accessToken);
  console.log('Roles:', decoded.roles);
  console.log('Permissions:', decoded.permissions);
}

4. Validación adicional durante la decodificación

Si necesitas validar el token durante la decodificación:

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    try {
      const decoded = jwtDecode(token);

      // Validaciones adicionales
      if (!decoded.sub) {
        throw new Error('Token missing subject');
      }

      if (decoded.iss !== 'https://my-auth-server.com') {
        throw new Error('Invalid token issuer');
      }

      return decoded;
    } catch (error) {
      console.error('Token decode error:', error);
      return {};
    }
  },
});

5. Manejo de errores personalizado

Si quieres un manejo de errores más específico:

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    try {
      const parts = token.split('.');
      if (parts.length !== 3) {
        throw new Error('Invalid token format');
      }

      const payload = JSON.parse(atob(parts[1]));
      return payload;
    } catch (error) {
      // Log para debugging
      console.error('Failed to decode token:', error);
      // Retornar objeto vacío para que la librería use expiresIn/issuedAt
      return {};
    }
  },
});

Comportamiento por Defecto

Si no proporcionas un tokenDecoder, la librería usa un decodificador básico que:

  1. Divide el token por . (formato JWT estándar: header.payload.signature)
  2. Decodifica el payload (segunda parte) desde base64
  3. Extrae exp e iat del payload decodificado
  4. Retorna un objeto vacío si hay algún error
// Decodificador por defecto (simplificado)
function defaultTokenDecoder(token: string) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) {
      return {};
    }
    const payload = parts[1];
    const decoded = JSON.parse(
      atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
    );
    return decoded;
  } catch {
    return {};
  }
}

¿Cuándo NO necesitas un Token Decoder personalizado?

No necesitas un decodificador personalizado si:

  • ✅ Tu backend genera JWTs estándar
  • ✅ El backend siempre envía expiresIn en la respuesta del login
  • ✅ No necesitas acceder a información adicional del token
  • ✅ El decodificador por defecto funciona correctamente

En estos casos, simplemente no proporciones tokenDecoder y la librería usará el decodificador por defecto.

Ejemplo Completo

import { AuthSession } from '@komarcalabs/auth-session';
import jwtDecode from 'jwt-decode';

const authSession = new AuthSession({
  // Usar jwt-decode para mejor manejo de errores
  tokenDecoder: (token) => {
    try {
      return jwtDecode(token);
    } catch (error) {
      console.error('Error decoding token:', error);
      return {};
    }
  },
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    const data = await response.json();
    return {
      accessToken: data.accessToken,
      refreshToken: data.refreshToken,
      expiresIn: data.expiresIn,
    };
  },
});

// Acceder a información adicional del token
function getUserRoles(): string[] {
  const session = authSession.getSession();
  if (!session) return [];

  const decoded = authSession.options.tokenDecoder(session.tokens.accessToken);
  return decoded.roles || [];
}

Resumen

| Caso | ¿Necesitas tokenDecoder? | | ----------------------------------------------- | ------------------------- | | JWT estándar con expiresIn del backend | ❌ No | | JWT estándar sin expiresIn | ❌ No (usa exp del JWT) | | Quieres usar jwt-decode | ✅ Opcional (recomendado) | | Token con formato no estándar | ✅ Sí | | Necesitas extraer información adicional | ✅ Sí | | Validación personalizada durante decodificación | ✅ Sí |


Estrategias de Almacenamiento

La librería soporta tres estrategias diferentes para almacenar los tokens de autenticación. Cada una tiene sus ventajas y casos de uso específicos.

localStorage (por defecto)

Los tokens persisten entre sesiones del navegador. Se mantienen incluso después de cerrar y abrir el navegador.

Características:

  • ✅ Persistencia entre sesiones
  • ✅ Los tokens sobreviven a reinicios del navegador
  • ✅ Ideal para aplicaciones que quieren mantener al usuario logueado
  • ⚠️ Los tokens persisten hasta que se eliminen explícitamente o el usuario limpie el almacenamiento

Uso:

const authSession = new AuthSession({
  storageStrategy: 'localStorage',
});

Casos de uso:

  • Aplicaciones web tradicionales
  • Cuando quieres que el usuario permanezca logueado entre sesiones
  • Aplicaciones donde la seguridad no es crítica (los tokens pueden ser accesibles por JavaScript)

sessionStorage

Los tokens se eliminan automáticamente al cerrar la pestaña o ventana del navegador.

Características:

  • ✅ Más seguro que localStorage (se elimina al cerrar la pestaña)
  • ✅ Los tokens no persisten entre sesiones
  • ✅ Ideal para aplicaciones que requieren re-autenticación frecuente
  • ⚠️ Si el usuario abre múltiples pestañas, cada una tiene su propia sesión

Uso:

const authSession = new AuthSession({
  storageStrategy: 'sessionStorage',
});

Casos de uso:

  • Aplicaciones bancarias o financieras
  • Aplicaciones con datos sensibles
  • Cuando quieres forzar logout al cerrar la pestaña
  • Aplicaciones donde la seguridad es crítica

memory

Los tokens solo existen en memoria (RAM). Se pierden al recargar la página o cerrar la aplicación.

Características:

  • ✅ Máxima seguridad (no se persiste en disco)
  • ✅ Útil para SSR (Server-Side Rendering)
  • ✅ Ideal para testing
  • ⚠️ Los tokens se pierden al recargar la página
  • ⚠️ No persiste entre navegaciones

Uso:

const authSession = new AuthSession({
  storageStrategy: 'memory',
});

Casos de uso:

  • Testing y desarrollo

  • Aplicaciones SSR (Next.js, Nuxt, etc.)

  • Cuando necesitas evitar almacenamiento persistente

  • Aplicaciones que requieren autenticación en cada carga

  • Aplicaciones que requieren autenticación en cada carga

cookie

Almacena los tokens en document.cookie. Permite que el servidor (SSR) acceda a los tokens y compartir sesión entre subdominios.

Características:

  • ✅ Soporte para SSR (Next.js, etc.)
  • ✅ Permite compartir sesión entre subdominios (.ejemplo.com)
  • ⚠️ Tamaño limitado (4KB)
  • ⚠️ Se envía en cada petición HTTP al servidor (overhead)

Uso:

const authSession = new AuthSession({
  storageStrategy: 'cookie',
  cookieOptions: {
    domain: '.mi-sitio.com', // Compartir entre subdominios
    secure: true, // Solo HTTPS
    sameSite: 'Lax',
  },
});

Casos de uso:

  • Renderizado del lado del servidor (SSR)
  • Compartir login entre app.dominio.com y blog.dominio.com

Comparación

| Característica | localStorage | sessionStorage | memory | cookie | | --------------------------- | -------------- | -------------------- | ------- | --------------- | | Persistencia entre sesiones | ✅ Sí | ❌ No | ❌ No | ✅ Sí | | Persistencia entre pestañas | ✅ Sí | ✅ Sí (misma sesión) | ❌ No | ✅ Sí | | Soporte Subdominios | ❌ No | ❌ No | ❌ No | ✅ Sí | | Acceso Servidor (SSR) | ❌ No | ❌ No | ❌ No | ✅ Sí | | Casos de uso | Apps generales | Apps sensibles | Testing | SSR/Subdominios |

Cambiar de Estrategia

Puedes cambiar la estrategia de almacenamiento en cualquier momento creando una nueva instancia:

// Cambiar de localStorage a sessionStorage
const authSession = new AuthSession({
  storageStrategy: 'sessionStorage',
});

Nota: Si cambias la estrategia, los tokens almacenados con la estrategia anterior no serán accesibles. El usuario necesitará hacer login nuevamente.

Ejemplo: Selección Dinámica

// Seleccionar estrategia basada en el entorno
const storageStrategy =
  process.env.NODE_ENV === 'test'
    ? 'memory'
    : process.env.NODE_ENV === 'development'
    ? 'sessionStorage'
    : 'localStorage';

const authSession = new AuthSession({
  storageStrategy,
});

Recomendaciones

Para Producción:

  • Aplicaciones generales: Usa localStorage para mejor UX
  • Aplicaciones con datos sensibles: Usa sessionStorage para mayor seguridad
  • Aplicaciones bancarias/financieras: Considera sessionStorage o incluso memory con re-autenticación frecuente

Para Desarrollo:

  • Usa memory o sessionStorage para evitar tokens persistentes durante el desarrollo

Para Testing:

  • Siempre usa memory para evitar interferencias entre tests
describe('Auth tests', () => {
  it('should login', async () => {
    const authSession = new AuthSession({
      storageStrategy: 'memory', // Aislar cada test
    });
    // ...
  });
});

Ejemplos de Configuración de Estrategias

Este documento proporciona ejemplos prácticos de cómo configurar @komarcalabs/auth-session para diferentes tipos de aplicaciones y requisitos de negocio.

Escenario 1: Aplicación Web Estándar (SPA/SaaS)

Objetivo: Mantener al usuario logueado indefinidamente (hasta que haga logout) para una buena experiencia de usuario.

// Configuración por defecto: usa localStorage
const authSession = new AuthSession({
  storageStrategy: 'localStorage',
  // Configurar expiración larga por defecto si el backend lo permite
  defaultExpiresIn: 24 * 60 * 60, // 24 horas
});

Escenario 2: Aplicación Bancaria o de Alta Seguridad

Objetivo: La sesión debe cerrarse inmediatamente si el usuario cierra la pestaña.

const bankingSession = new AuthSession({
  storageStrategy: 'sessionStorage',
  // Margen de seguridad para renovar tokens antes
  refreshMargin: 60, // 1 minuto antes
});

Escenario 3: Plataforma Multi-Subdominio (SaaS)

Objetivo: El usuario hace login en app.empresa.com y mantiene la sesión en dashboard.empresa.com y ayuda.empresa.com.

Solución: Usar cookie con configuración de dominio wildcard.

const platformSession = new AuthSession({
  storageStrategy: 'cookie',
  cookieOptions: {
    domain: '.empresa.com', // El punto al inicio permite compartir con subdominios
    path: '/', // Disponible en toda la ruta
    secure: true, // Solo HTTPS (Recomendado)
    sameSite: 'Lax', // Protección CSRF balanceada
  },
});

Escenario 4: Server Side Rendering (Next.js)

Objetivo: Que el token esté disponible tanto en el servidor (para renderizado inicial) como en el cliente.

// utils/auth.ts
export const authSession = new AuthSession({
  storageStrategy: 'cookie', // Las cookies se envían automáticamente al servidor
  cookieOptions: {
    sameSite: 'Lax',
    path: '/',
  },
});

Escenario 5: Entorno de Pruebas (Unit Testing)

Objetivo: Aislar cada test para que no compartan estado.

// test/setup.ts
import { AuthSession } from '@komarcalabs/auth-session';

beforeEach(() => {
  // Crear una instancia nueva y limpia para cada test
  const testSession = new AuthSession({
    storageStrategy: 'memory',
  });
});

Ejemplos de Uso

Ejemplo Completo: Servicio de Autenticación

Aquí tienes un ejemplo completo de cómo integrar la librería con tu backend:

import { AuthSession } from '@komarcalabs/auth-session';
import type { AuthTokens, User } from '@komarcalabs/auth-session';

class AuthService {
  private authSession: AuthSession;
  private apiBaseUrl: string;

  constructor(apiBaseUrl: string) {
    this.apiBaseUrl = apiBaseUrl;

    // Usamos el tipo User genérico para mayor seguridad
    this.authSession = new AuthSession<User>({
      storageStrategy: 'localStorage',
      onRefreshToken: async (refreshToken) => {
        // El backend renueva el token
        return this.refreshTokenFromBackend(refreshToken);
      },
    });
  }

  /**
   * Login: El backend valida credenciales y devuelve JWT
   */
  async login(email: string, password: string): Promise<void> {
    const response = await fetch(`${this.apiBaseUrl}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Login failed');
    }

    // El backend envía los tokens (JWT)
    const data = await response.json();
    // {
    //   accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    //   refreshToken: "refresh_token_string",
    //   expiresIn: 3600,
    //   user: { id: "123", email: "[email protected]", ... }
    // }

    await this.authSession.login(
      {
        accessToken: data.accessToken, // JWT del backend
        refreshToken: data.refreshToken,
        expiresIn: data.expiresIn,
      },
      data.user
    );
  }

  /**
   * Refresh: Solicita nuevos tokens al backend
   */
  private async refreshTokenFromBackend(
    refreshToken: string
  ): Promise<AuthTokens> {
    const response = await fetch(`${this.apiBaseUrl}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Failed to refresh token');
    }

    // El backend devuelve nuevos tokens
    const data = await response.json();
    return {
      accessToken: data.accessToken, // Nuevo JWT del backend
      refreshToken: data.refreshToken,
      expiresIn: data.expiresIn,
    };
  }

  /**
   * Logout: Limpia la sesión local
   */
  async logout(): Promise<void> {
    // Opcional: notificar al backend
    try {
      const token = await this.authSession.getAccessToken();
      if (token) {
        await fetch(`${this.apiBaseUrl}/auth/logout`, {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${token}`,
          },
        });
      }
    } catch {
      // Ignorar errores de logout en backend
    }

    await this.authSession.logout();
  }

  /**
   * Obtener token para usar en peticiones
   */
  async getAccessToken(): Promise<string | null> {
    return this.authSession.getAccessToken();
  }

  /**
   * Verificar si está autenticado
   */
  isAuthenticated(): boolean {
    return this.authSession.isAuthenticated();
  }

  /**
   * Obtener usuario
   */
  getUser(): User | null {
    return this.authSession.getUser();
  }
}

// Uso
const authService = new AuthService('https://api.example.com');

// Login
await authService.login('[email protected]', 'password');

// Hacer peticiones autenticadas
const token = await authService.getAccessToken();
const response = await fetch('https://api.example.com/protected', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

Integración con React

import { useEffect, useState } from 'react';
import { AuthSession } from '@komarcalabs/auth-session';

const authSession = new AuthSession({
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    return response.json();
  },
});

function useAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(
    authSession.isAuthenticated()
  );
  const [user, setUser] = useState(authSession.getUser());

  useEffect(() => {
    const unsubscribeLogin = authSession.on('login', () => {
      setIsAuthenticated(true);
      setUser(authSession.getUser());
    });

    const unsubscribeLogout = authSession.on('logout', () => {
      setIsAuthenticated(false);
      setUser(null);
    });

    const unsubscribeExpired = authSession.on('expired', () => {
      setIsAuthenticated(false);
      setUser(null);
      // Opcional: redirigir a login
      window.location.href = '/login';
    });

    return () => {
      unsubscribeLogin();
      unsubscribeLogout();
      unsubscribeExpired();
    };
  }, []);

  return { isAuthenticated, user, authSession };
}

// Uso en componente
function App() {
  const { isAuthenticated, user, authSession } = useAuth();

  if (!isAuthenticated) {
    return <LoginForm />;
  }

  return (
    <div>
      <h1>Bienvenido, {user?.name}</h1>
      <button onClick={() => authSession.logout()}>Cerrar Sesión</button>
    </div>
  );
}

Interceptor de Fetch con Renovación Automática

const authSession = new AuthSession({
  onRefreshToken: async (refreshToken) => {
    // Tu lógica de refresh
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    return response.json();
  },
});

async function authenticatedFetch(url: string, options: RequestInit = {}) {
  const token = await authSession.getAccessToken();

  if (!token) {
    throw new Error('No authentication token available');
  }

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${token}`,
    },
  });

  // Si el token expiró, intentar renovar y reintentar
  if (response.status === 401) {
    try {
      await authSession.refresh();
      const newToken = await authSession.getAccessToken();

      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${newToken}`,
        },
      });
    } catch {
      // Si falla el refresh, redirigir a login
      await authSession.logout();
      window.location.href = '/login';
      throw new Error('Session expired');
    }
  }

  return response;
}

Validación Personalizada de Sesión

const authSession = new AuthSession({
  onValidateSession: async (session) => {
    // Verificar con el servidor que la sesión sigue siendo válida
    try {
      const response = await fetch('/api/auth/validate', {
        headers: {
          Authorization: `Bearer ${session.tokens.accessToken}`,
        },
      });
      return response.ok;
    } catch {
      return false;
    }
  },
});

// Validar periódicamente
setInterval(async () => {
  const isValid = await authSession.validateSession();
  if (!isValid) {
    // Redirigir a login
    window.location.href = '/login';
  }
}, 5 * 60 * 1000); // Cada 5 minutos

Decodificador de JWT Personalizado

import jwtDecode from 'jwt-decode';

const authSession = new AuthSession({
  tokenDecoder: (token) => {
    return jwtDecode(token);
  },
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    return response.json();
  },
});

// Acceder a información adicional del token
// Guarda una referencia al decoder para reutilizarlo
const tokenDecoder = (token: string) => jwtDecode(token);

const authSession = new AuthSession({
  tokenDecoder,
  onRefreshToken: async (refreshToken) => {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
    return response.json();
  },
});

function getUserRoles(): string[] {
  const session = authSession.getSession();
  if (!session) return [];

  const decoded = tokenDecoder(session.tokens.accessToken);
  return decoded.roles || [];
}

Integración con Axios

import axios from 'axios';
import { AuthSession } from '@komarcalabs/auth-session';

const authSession = new AuthSession({
  onRefreshToken: async (refreshToken) => {
    const response = await axios.post('/api/auth/refresh', { refreshToken });
    return response.data;
  },
});

// Interceptor de axios
axios.interceptors.request.use(async (config) => {
  const token = await authSession.getAccessToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Interceptor de respuesta para manejar 401
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      try {
        await authSession.refresh();
        // Reintentar la petición original
        const token = await authSession.getAccessToken();
        error.config.headers.Authorization = `Bearer ${token}`;
        return axios.request(error.config);
      } catch {
        await authSession.logout();
        window.location.href = '/login';
      }
    }
    return Promise.reject(error);
  }
);

Testing

import { AuthSession } from '@komarcalabs/auth-session';

  });
});

## Uso con Tokens No-JWT (Opacos)

La librería soporta tokens que no son JWT (por ejemplo, strings aleatorios de sesión). El decodificador por defecto no fallará. Solo asegúrate de proporcionar `expiresIn` al hacer login.

```typescript
// No necesitas configuración especial, el decoder por defecto manejará strings simples retornando {}
const authSession = new AuthSession({
  // ... configuración estándar
});

await authSession.login({
  accessToken: 'session_id_aleatorio_7382',
  refreshToken: 'refresh_token_aleatorio_9921',
  expiresIn: 3600, // IMPORTANTE: Definir tiempo de expiración explícitamente
});

Ciclo de Vida de la Autenticación

Este documento explica paso a paso el flujo de trabajo recomendado al usar @komarcalabs/auth-session, aclarando cómo y cuándo interactuar con la librería.

1. Login (Inicio de Sesión)

El ciclo comienza cuando el usuario se autentica en tu aplicación.

Lo que tú haces:

  1. Recolectas credenciales (usuario/password) en tu UI.
  2. Las envías a tu backend.
  3. Recibes los tokens (accessToken, refreshToken).
  4. Se los pasas a la librería.
// Tu función de login
async function handleLogin(credentials) {
  // 1. Petición al backend
  const response = await api.post('/login', credentials);
  const { accessToken, refreshToken, expiresIn, user } = response.data;

  // 2. Guardar en la librería (Síncrono)
  await authSession.login({ accessToken, refreshToken, expiresIn }, user);

  // 3. Redirigir al usuario
  router.push('/dashboard');
}

2. Consumo de APIs (El Interceptor)

Aquí es donde ocurre la magia. No deberías estar verificando tokens manualmente en cada componente. En su lugar, usa un "Interceptor" en tu cliente HTTP (Axios, Fetch, etc.).

¿Por qué getAccessToken() es asíncrono?

Puede parecer extraño tener que hacer await auth.getAccessToken(), pero es una decisión de diseño crítica por eficiencia y seguridad:

  1. Eficiencia: La función primero verifica en memoria si el token es válido. Esto toma microsegundos.
  2. Seguridad: Si (y solo si) el token está por vencer, la librería pausa la ejecución, renueva el token en segundo plano, y luego te devuelve el nuevo.

¿Es costoso? ¡No! En el 99% de los casos, la función retorna inmediatamente el token guardado. Solo se hace lenta cuando realmente necesita renovar el token.

Ejemplo con Axios

Este es el patrón estándar de oro. Configúralo una vez y olvídate.

import axios from 'axios';
import { authSession } from './auth';

const api = axios.create({ baseURL: '/api' });

api.interceptors.request.use(async (config) => {
  // 1. Obtener token (Automáticamente maneja renovación si es necesario)
  const token = await authSession.getAccessToken();

  // 2. Inyectar en headers
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

Ejemplo con Fetch

Si usas fetch, puedes crear un wrapper simple:

async function authFetch(url, options = {}) {
  // 1. Asegurar token
  const token = await authSession.getAccessToken();

  // 2. Preparar headers
  const headers = {
    ...options.headers,
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
  };

  // 3. Ejecutar
  return fetch(url, { ...options, headers });
}

3. Renovación Automática (Auto-Refresh)

La librería maneja esto internamente, pero es útil entender qué pasa.

  1. La librería detecta que el token expirará pronto (basado en refreshMargin).
  2. Llama a tu callback onRefreshToken.
  3. Si hay 10 peticiones simultáneas, la librería solo hace 1 refresh y comparte el resultado con todas.
  4. Si el refresh falla (ej. refresh token inválido), el estado pasa a expired o unauthenticated.

4. Expiración de Sesión

Si el refresh falla o el token expira irremediablemente, la sesión "muere". Debes escuchar el evento para redirigir al usuario al login.

// En tu archivo principal (main.ts / App.tsx)
authSession.on('expired', () => {
  console.warn('La sesión expiró. Redirigiendo al login...');
  router.push('/login');
});

Nota sobre Sesiones Infinitas: Si configuras expiresIn: null (o 0), este evento nunca se disparará automáticamente por tiempo. Solo se disparará si el servidor rechaza un token (ej. 401 Unauthorized) y tú o la librería lo marcan explícitamente como expirado.

5. Logout

Cuando el usuario decide salir explícitamente.

async function handleLogout() {
  await authSession.logout();
  router.push('/login');
}

Resumen de Mejores Prácticas

  1. Siempre usa getAccessToken() antes de cada petición protegida.
  2. Centraliza esa lógica en un interceptor o wrapper HTTP.
  3. Configura onRefreshToken si tu backend soporta refresh tokens.
  4. Escucha el evento expired para manejar desconexiones forzadas.