npm package: @komarcalabs/auth-session
Description: <a id="komarcalabsauth-session"></a>
Latest version: 0.1.7

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

# @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

```bash
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`](https://gitlab.com/komarca/komarca-libs/auth-session/-/tree/main/docs):

### 📚 Índice de Documentación

- **[Guía de Inicio Rápido](#guia-de-inicio-rapido)**

  - 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](#ciclo-de-vida-de-la-autenticacion)**

  - Entendiendo `getAccessToken` (Síncrono vs Asíncrono)
  - Implementación con Interceptores (Axios/Fetch)
  - Renovación automática y expiración

- **[Referencia de API](#referencia-de-api)**

  - Métodos principales
  - Sistema de eventos
  - Tipos TypeScript
  - Interfaces y tipos exportados

- **[Configuración](#configuracion)**

  - Opciones de configuración detalladas
  - Explicación de cada opción
  - Ejemplos de configuración

- **[Token Decoder](#token-decoder)**

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

- **[Estrategias de Almacenamiento](#estrategias-de-almacenamiento)**

  - localStorage, sessionStorage, memory, cookie
  - Comparación y recomendaciones

- **[Ejemplos por Escenarios](#ejemplos-de-configuracion-de-estrategias)**

  - Apps Bancarias
  - Multi-Subdominio
  - SSR (Next.js)
  - Testing

- **[Ejemplos de Uso](#ejemplos-de-uso)**
  - Servicio de autenticación completo
  - Integración con React
  - Interceptor de Fetch
  - Integración con Axios
  - Testing

## Inicio Rápido

```typescript
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.


---

<a id="guia-de-inicio-rapido"></a>

# Guía de Inicio Rápido

## Instalación

```bash
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

```typescript
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: "user@example.com", 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:

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

## Uso Básico

### Configuración Inicial

```typescript
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

```typescript
// 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: 'usuario@example.com',
  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`

   ```typescript
   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:

   ```typescript
   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

```typescript
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

```typescript
// 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

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

### Logout

```typescript
await authSession.logout();
```

### Renovación Manual de Token

```typescript
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
}
```


---

<a id="referencia-de-api"></a>

# 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.

```typescript
await authSession.login(
  {
    accessToken: 'token',
    refreshToken: 'refresh',
    expiresIn: 3600,
  },
  {
    id: '123',
    email: 'user@example.com',
  }
);
```

### `logout(): Promise<void>`

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

```typescript
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.

```typescript
const token = await authSession.getAccessToken();
```

### `refresh(): Promise<AuthTokens>`

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

```typescript
const newTokens = await authSession.refresh();
```

### `isAuthenticated(): boolean`

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

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

### `getUser(): User | null`

Obtiene la información del usuario actual.

```typescript
const user = authSession.getUser();
```

### `updateUser(user: Partial<User>): void`

Actualiza parcialmente la información del usuario.

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

### `getSession(): SessionData | null`

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

```typescript
const session = authSession.getSession();
```

### `validateSession(): Promise<boolean>`

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

```typescript
const isValid = await authSession.validateSession();
```

### `getStatus(): SessionStatus`

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

```typescript
const status = authSession.getStatus();
```

### `getRefreshToken(): string | null`

Obtiene el refresh token actual.

```typescript
const refreshToken = authSession.getRefreshToken();
```

## Sistema de Eventos

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

```typescript
// 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:

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

### `AuthTokens`

```typescript
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`

```typescript
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`

```typescript
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`

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

### `StorageStrategy`

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


---

<a id="configuracion"></a>

# Configuración

## Opciones de Configuración

### `AuthSessionOptions`

```typescript
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](./storage-strategies.md) 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](./token-decoder.md) 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

```typescript
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()`.

```typescript
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

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

### Opción B: Por Login

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

---

## Ejemplo de Configuración Completa

```typescript
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;
  },
});
```


---

<a id="token-decoder"></a>

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

```typescript
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):

```typescript
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.):

```typescript
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:

```typescript
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:

```typescript
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

```typescript
// 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

```typescript
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í                     |


---

<a id="estrategias-de-almacenamiento"></a>

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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
// 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

```typescript
// 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

```typescript
describe('Auth tests', () => {
  it('should login', async () => {
    const authSession = new AuthSession({
      storageStrategy: 'memory', // Aislar cada test
    });
    // ...
  });
});
```


---

<a id="ejemplos-de-configuracion-de-estrategias"></a>

# 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.

```typescript
// 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.

```typescript
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.

```typescript
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.

```typescript
// 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.

```typescript
// 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',
  });
});
```


---

<a id="ejemplos-de-uso"></a>

# Ejemplos de Uso

## Ejemplo Completo: Servicio de Autenticación

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

```typescript
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: "user@example.com", ... }
    // }

    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('user@example.com', '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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

````typescript
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
});
````

```

```


---

<a id="ciclo-de-vida-de-la-autenticacion"></a>

# 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.

```typescript
// 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.

```typescript
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:

```typescript
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.

```typescript
// 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.

```typescript
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.