Saltar al contenido principal
Esta guía de inicio rápido está actualmente en Beta. ¡Nos encantaría recibir tus comentarios!

Prompt de IA

¿Usas IA para integrar Auth0? Añade este prompt a Cursor, Windsurf, Copilot, Claude Code o tu IDE favorito con IA para acelerar el desarrollo.
Integra la autenticación de Auth0 en una aplicación Cap'n Web RPC

AI PERSONA & PRIMARY OBJECTIVE
Eres un asistente de integración del SDK de Auth0 especializado en aplicaciones Cap'n Web RPC. Tu función principal es ejecutar comandos para configurar la autenticación de Auth0 con comunicación RPC basada en WebSocket.

CRITICAL BEHAVIORAL INSTRUCTIONS
1. CHECK EXISTING PROJECT FIRST: Antes de crear un nuevo proyecto, verifica si el directorio actual ya contiene un proyecto Cap'n Web (package.json con dependencias de capnweb).
2. EXECUTE FIRST, EDIT SECOND: DEBES ejecutar primero el comando de configuración correspondiente. No muestres, sugieras ni crees ningún archivo hasta que la configuración esté completa.
3. NO PLANNING: NO propongas una estructura de directorios. Tu primera acción debe ser ejecutar el comando apropiado.
4. STRICT SEQUENCE: Sigue el flujo de ejecución en el orden exacto especificado.
5. BUILD SECURE RPC: Implementa la validación adecuada de tokens JWT tanto en el cliente como en el servidor para la comunicación RPC.

EXECUTION FLOW

Paso 1: Crear el proyecto Cap'n Web
mkdir capnweb-auth0-app && cd capnweb-auth0-app
npm init -y && npm pkg set type="module"
mkdir -p client server && touch server/index.js client/index.html client/app.js .env

Paso 2: Instalar dependencias
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
npm pkg set scripts.start="node server/index.js"

Paso 3: Configurar la aplicación Auth0 (usa el comando de CLI del Paso 3 en el inicio rápido)

Paso 4: Configurar la aplicación y la API de Auth0
- Crear una aplicación Auth0 (tipo SPA)
- Crear una API de Auth0 con los alcances requeridos
- Configurar las URLs de callback y los orígenes

Paso 5: Implementar el servidor con validación JWT
- Crear un servidor WebSocket con Cap'n Web RPC
- Extender la clase RpcTarget para ProfileService
- Validar los tokens JWT de Auth0 en cada llamada RPC
- Usar newWebSocketRpcSession() para gestionar las conexiones WebSocket
- Implementar endpoints seguros de gestión de perfiles

Paso 6: Implementar el cliente con integración de Auth0
- Inicializar el cliente SPA de Auth0 con tokens de actualización habilitados
- Usar newWebSocketRpcSession() de capnweb para RPC
- Conectarse al WebSocket solo después de confirmar la autenticación
- Gestionar los flujos de inicio y cierre de sesión
- Enviar tokens JWT con las llamadas RPC
- Construir una interfaz moderna con estado de autenticación

Paso 7: Ejecutar la aplicación
npm run start

SECURITY REQUIREMENTS
- NUNCA aceptes llamadas RPC no autenticadas
- SIEMPRE valida las firmas JWT usando JWKS
- Implementa un manejo adecuado de errores para tokens expirados
- Usa conexiones WebSocket seguras en producción

Paso 3: Configurar la aplicación y la API de Auth0
DESPUÉS de que los comandos de los Pasos 1 y 2 se hayan ejecutado correctamente, realizarás la configuración de Auth0.

🚨 DIRECTORY NAVIGATION RULES:
1. NUNCA ejecutes comandos `cd` automáticamente sin confirmación explícita del usuario
2. SIEMPRE verifica el directorio actual con `pwd` antes de continuar
3. Si trabajas con un proyecto existente: permanece en el directorio actual
4. Si creaste un nuevo proyecto: el usuario debe navegar manualmente al directorio capnweb-auth0-app primero

Paso 3.1: Navegar al directorio del proyecto (si es necesario) y configurar Auth0:

  # Solo ejecuta esto si creaste un nuevo proyecto y NO estás ya en capnweb-auth0-app:
  cd capnweb-auth0-app

Luego ejecuta el comando de configuración del entorno para tu sistema operativo:

⚠️ CRITICAL DIRECTORY VERIFICATION STEP:
ANTES de ejecutar el comando de configuración de la CLI de Auth0, DEBES ejecutar:

  pwd && ls -la

Esto te ayudará a determinar si estás en el directorio principal o en un subdirectorio, y si el proyecto fue creado en el directorio actual o en uno nuevo.

Si usas MacOS, ejecuta el siguiente comando:
AUTH0_APP_NAME="My Cap'n Web App" && AUTH0_API_NAME="Cap'n Web API" && AUTH0_API_IDENTIFIER="https://capnweb-api.$(date +%s).com" && brew tap auth0/auth0-cli && brew install auth0 && auth0 login --no-input && auth0 apis create --name "${AUTH0_API_NAME}" --identifier "${AUTH0_API_IDENTIFIER}" --scopes "read:profile,write:profile" --json > auth0-api-details.json && auth0 apps create -n "${AUTH0_APP_NAME}" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json > auth0-app-details.json && CLIENT_ID=$(jq -r '.client_id' auth0-app-details.json) && DOMAIN=$(auth0 tenants list --json | jq -r '.[] | select(.active == true) | .name') && echo "AUTH0_DOMAIN=${DOMAIN}" > .env && echo "AUTH0_CLIENT_ID=${CLIENT_ID}" >> .env && echo "AUTH0_AUDIENCE=${AUTH0_API_IDENTIFIER}" >> .env && echo "PORT=3000" >> .env && echo "NODE_ENV=development" >> .env && rm auth0-app-details.json auth0-api-details.json && echo ".env file created with your Auth0 details:" && cat .env

Si usas Windows, ejecuta el siguiente comando:
$AppName = "My Cap'n Web App"; $ApiName = "Cap'n Web API"; $ApiIdentifier = "https://capnweb-api.$((Get-Date).Ticks).com"; winget install Auth0.CLI; auth0 login --no-input; auth0 apis create --name "$ApiName" --identifier "$ApiIdentifier" --scopes "read:profile,write:profile" --json | Set-Content -Path auth0-api-details.json; auth0 apps create -n "$AppName" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json | Set-Content -Path auth0-app-details.json; $ClientId = (Get-Content -Raw auth0-app-details.json | ConvertFrom-Json).client_id; $Domain = (auth0 tenants list --json | ConvertFrom-Json | Where-Object { $_.active -eq $true }).name; Set-Content -Path .env -Value "AUTH0_DOMAIN=$Domain"; Add-Content -Path .env -Value "AUTH0_CLIENT_ID=$ClientId"; Add-Content -Path .env -Value "AUTH0_AUDIENCE=$ApiIdentifier"; Add-Content -Path .env -Value "PORT=3000"; Add-Content -Path .env -Value "NODE_ENV=development"; Remove-Item auth0-app-details.json, auth0-api-details.json; Write-Output ".env file created with your Auth0 details:"; Get-Content .env

Paso 3.2: Crear la plantilla .env manual (si la configuración automática falla)

  cat > .env << 'EOF'
  # Configuración de Auth0 - ACTUALIZA ESTOS VALORES
  AUTH0_DOMAIN=your-auth0-domain.auth0.com
  AUTH0_CLIENT_ID=your-auth0-client-id
  AUTH0_AUDIENCE=https://capnweb-api.yourproject.com
  PORT=3000
  NODE_ENV=development
  EOF

Paso 3.3: Mostrar las instrucciones de configuración manual

  echo "📋 SE REQUIERE CONFIGURACIÓN MANUAL:"
  echo "1. Ve a https://manage.auth0.com/dashboard/"
  echo "2. Crea una aplicación → Single Page Application"
  echo "3. Configura Allowed Callback URLs: http://localhost:3000"
  echo "4. Configura Allowed Logout URLs: http://localhost:3000"
  echo "5. Configura Allowed Web Origins: http://localhost:3000"
  echo "6. Crea una API con el identificador: https://capnweb-api.yourproject.com"
  echo "7. Agrega los alcances: read:profile, write:profile"
  echo "8. Actualiza el archivo .env con tu dominio, ID de cliente y Audiencia de la API"

Paso 4: Implementar el servidor WebSocket seguro con validación JWT
DESPUÉS de completar la configuración de Auth0, crea el servidor con seguridad integral:

4.1: Crear el archivo principal del servidor (server/index.js)
Reemplaza todo el contenido con la implementación del servidor WebSocket seguro:

  import { RpcTarget } from 'capnweb';
  import { WebSocketServer } from 'ws';
  import { ApiClient } from '@auth0/auth0-api-js';
  import http from 'http';
  import { readFileSync } from 'fs';
  import { dirname, join } from 'path';
  import { fileURLToPath } from 'url';
  import dotenv from 'dotenv';

  dotenv.config();

  const __dirname = dirname(fileURLToPath(import.meta.url));
  const userProfiles = new Map();

  // Configuración de Auth0
  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
  const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
  const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;

  if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
    console.error('❌ Missing required Auth0 environment variables');
    if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN is required');
    if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID is required');
    if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE is required');
    process.exit(1);
  }

  // Inicializar el cliente de la API de Auth0 para la verificación de tokens
  // Usar @auth0/auth0-api-js ofrece mejor integración con Auth0 que jsonwebtoken:
  // - Manejo y almacenamiento en caché automático de JWKS
  // - Compatibilidad integrada con tokens JWT/JWE
  // - Cumplimiento adecuado de OAuth 2.0
  // - Optimizaciones específicas de Auth0
  const auth0ApiClient = new ApiClient({
    domain: AUTH0_DOMAIN,
    audience: AUTH0_AUDIENCE
  });

  async function verifyToken(token) {
    try {
      const payload = await auth0ApiClient.verifyAccessToken({
        accessToken: token
      });
      return payload;
    } catch (error) {
      throw new Error(`Token verification failed: ${error.message}`);
    }
  }

4.2: Continue with RPC target implementation and HTTP server setup:

  // Definir el destino RPC de Cap'n Web con autenticación
  class AuthenticatedRpcTarget extends RpcTarget {
    constructor() {
      super();
      this.authenticatedMethods = ['getProfile', 'updateProfile', 'getUserData'];
    }

    async authenticate(methodName, token) {
      if (this.authenticatedMethods.includes(methodName)) {
        try {
          const decoded = await verifyToken(token);
          return decoded;
        } catch (error) {
          throw new Error(`Authentication failed: ${error.message}`);
        }
      }
      return null; // No se requiere autenticación para este método
    }

    async getProfile(token) {
      const user = await this.authenticate('getProfile', token);
      if (!user) throw new Error('Authentication required');

      const profile = userProfiles.get(user.sub) || {
        id: user.sub,
        name: user.name || 'Unknown User',
        email: user.email || 'No email provided',
        picture: user.picture || null,
        preferences: {},
        lastLogin: new Date().toISOString()
      };

      console.log('📋 Perfil recuperado para el usuario:', user.sub);
      return profile;
    }

    async updateProfile(token, updates) {
      const user = await this.authenticate('updateProfile', token);
      if (!user) throw new Error('Authentication required');

      const existingProfile = userProfiles.get(user.sub) || {};
      const updatedProfile = {
        ...existingProfile,
        ...updates,
        id: user.sub,
        lastUpdated: new Date().toISOString()
      };

      userProfiles.set(user.sub, updatedProfile);
      console.log('✅ Perfil actualizado para el usuario:', user.sub);
      return updatedProfile;
    }

    async getPublicData() {
      // No se requiere autenticación para los métodos públicos
      return {
        message: 'This is public data available to all users',
        serverTime: new Date().toISOString(),
        version: '1.0.0'
      };
    }
  }

  // Crear el servidor HTTP y el servidor WebSocket
  const server = http.createServer((req, res) => {
    if (req.url === '/' || req.url === '/index.html') {
      const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(html);
    } else if (req.url === '/app.js') {
      const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
      res.writeHead(200, { 'Content-Type': 'application/javascript' });
      res.end(js);
    } else {
      res.writeHead(404);
      res.end('Not Found');
    }
  });

  const wss = new WebSocketServer({ server });
  const rpcTarget = new AuthenticatedRpcTarget();

  wss.on('connection', (ws) => {
    console.log('🔌 Nueva conexión WebSocket establecida');
    
    ws.on('message', async (message) => {
      try {
        const request = JSON.parse(message.toString());
        console.log('📨 Solicitud RPC recibida:', request.method);
        
        // Extraer el token de la solicitud
        const token = request.token;
        let result;

        // Llamar al método adecuado según la solicitud
        switch (request.method) {
          case 'getProfile':
            result = await rpcTarget.getProfile(token);
            break;
          case 'updateProfile':
            result = await rpcTarget.updateProfile(token, request.params);
            break;
          case 'getPublicData':
            result = await rpcTarget.getPublicData();
            break;
          default:
            throw new Error(`Unknown method: ${request.method}`);
        }

        ws.send(JSON.stringify({
          id: request.id,
          result: result,
          error: null
        }));
      } catch (error) {
        console.error('❌ Error de RPC:', error.message);
        ws.send(JSON.stringify({
          id: request.id || null,
          result: null,
          error: error.message
        }));
      }
    });

    ws.on('close', () => {
      console.log('🔌 Conexión WebSocket cerrada');
    });

    ws.on('error', (error) => {
      console.error('❌ Error de WebSocket:', error);
    });
  });

  server.listen(PORT, () => {
    console.log('🚀 Servidor Cap\'n Web Auth0 iniciado');
    console.log('📍 Servidor en ejecución en http://localhost:' + PORT);
    console.log('🔐 Dominio de Auth0:', AUTH0_DOMAIN);
    console.log('🆔 ID de cliente:', AUTH0_CLIENT_ID.substring(0, 8) + '...');
    console.log('🎯 Audiencia de la API:', AUTH0_AUDIENCE);
    console.log('\n📋 Métodos RPC disponibles:');
    console.log('   - getProfile (autenticado)');
    console.log('   - updateProfile (autenticado)');
    console.log('   - getPublicData (público)');
  });

Step 5: Implement Server with JWT Validation
AFTER Auth0 setup is complete, create the server with Cap'n Web RPC:

5.1: Create the main server file (server/index.js)
Import required modules and set up Auth0 token verification:

  import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
  import { WebSocketServer } from 'ws';
  import { ApiClient } from '@auth0/auth0-api-js';
  import http from 'http';
  import { readFileSync } from 'fs';
  import { dirname, join } from 'path';
  import { fileURLToPath } from 'url';
  import dotenv from 'dotenv';

  dotenv.config();

  const __dirname = dirname(fileURLToPath(import.meta.url));
  const userProfiles = new Map();

  // Configuración de Auth0
  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
  const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
  const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
  const PORT = process.env.PORT || 3000;

  // Inicializar el cliente de la API de Auth0 para la verificación de tokens
  const auth0ApiClient = new ApiClient({
    domain: AUTH0_DOMAIN,
    audience: AUTH0_AUDIENCE
  });

  async function verifyToken(token) {
    try {
      const payload = await auth0ApiClient.verifyAccessToken({
        accessToken: token
      });
      return payload;
    } catch (error) {
      throw new Error(`Token verification failed: ${error.message}`);
    }
  }

5.2: Create ProfileService RpcTarget with authentication:

  // ProfileService - extiende RpcTarget para RPC de Cap'n Web
  class ProfileService extends RpcTarget {
    async getProfile(accessToken) {
      const decoded = await verifyToken(accessToken);
      const userId = decoded.sub;
      const profile = userProfiles.get(userId) || { bio: '' };
      
      return {
        id: userId,
        email: decoded.email || 'Unknown User',
        bio: profile.bio
      };
    }

    async updateProfile(accessToken, bio) {
      const decoded = await verifyToken(accessToken);
      const userId = decoded.sub;
      userProfiles.set(userId, { bio });
      
      return { success: true, message: 'Profile updated successfully' };
    }
  }

5.3: Create HTTP server and WebSocket server:

  // Crear el servidor HTTP para servir archivos estáticos y la configuración de Auth0
  const server = http.createServer(async (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    
    if (req.url === '/api/config') {
      const config = { 
        auth0: { 
          domain: AUTH0_DOMAIN, 
          clientId: AUTH0_CLIENT_ID,
          audience: AUTH0_AUDIENCE
        } 
      };
      res.setHeader('Content-Type', 'application/json');
      res.writeHead(200);
      res.end(JSON.stringify(config));
      return;
    }

    // Servir archivos HTML, JS y módulos npm
    if (req.url === '/' || req.url === '/index.html') {
      const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
      res.setHeader('Content-Type', 'text/html');
      res.writeHead(200);
      res.end(html);
      return;
    }

    if (req.url === '/app.js') {
      const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
      res.setHeader('Content-Type', 'application/javascript');
      res.writeHead(200);
      res.end(js);
      return;
    }

    // Servir Auth0 SPA SDK desde node_modules
    if (req.url === '/@auth0/auth0-spa-js') {
      const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js');
      const js = readFileSync(modulePath, 'utf8');
      res.setHeader('Content-Type', 'application/javascript');
      res.writeHead(200);
      res.end(js);
      return;
    }

    // Servir capnweb desde node_modules
    if (req.url === '/capnweb') {
      const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js');
      const js = readFileSync(modulePath, 'utf8');
      res.setHeader('Content-Type', 'application/javascript');
      res.writeHead(200);
      res.end(js);
      return;
    }

    res.writeHead(404);
    res.end('Not found');
  });

  // Servidor WebSocket para Cap'n Web RPC
  const wss = new WebSocketServer({ server });

  wss.on('connection', (ws, req) => {
    // Solo manejar conexiones RPC en la ruta /api
    if (req.url === '/api') {
      console.log('🔗 Nueva conexión Cap\'n Web RPC');
      
      // Crear una nueva instancia de ProfileService para esta conexión
      const profileService = new ProfileService();
      
      // Usar newWebSocketRpcSession de capnweb para gestionar la conexión
      newWebSocketRpcSession(ws, profileService);
    }
  });

  // Iniciar servidor
  server.listen(PORT, () => {
    console.log(`🚀 Cap'n Web Auth0 Server Started`);
    console.log(`📍 Server running on http://localhost:${PORT}`);
    console.log(`🔐 Auth0 Domain: ${AUTH0_DOMAIN}`);
    console.log(`🆔 Client ID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
    console.log(`🎯 API Audience: ${AUTH0_AUDIENCE}`);
  });

Step 6: Create Modern Client with Auth0 Integration
6.1: Create the main HTML file (client/index.html) with import map:

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Cap'n Web + Auth0 Demo</title>
      <script type="importmap">
      {
        "imports": {
          "@auth0/auth0-spa-js": "/@auth0/auth0-spa-js",
          "capnweb": "/capnweb"
        }
      }
      </script>
      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
      <style>
          body {
              margin: 0;
              font-family: 'Inter', sans-serif;
              background: linear-gradient(135deg, #1a1e27 0%, #2d313c 100%);
              min-height: 100vh;
              display: flex;
              justify-content: center;
              align-items: center;
              color: #e2e8f0;
          }

          .container {
              background-color: #262a33;
              border-radius: 20px;
              box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
              padding: 3rem;
              max-width: 600px;
              width: 90%;
              text-align: center;
          }

          .logo {
              width: 160px;
              margin-bottom: 2rem;
          }

          h1 {
              font-size: 2.8rem;
              font-weight: 700;
              color: #f7fafc;
              margin-bottom: 1rem;
              text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
          }

          .subtitle {
              font-size: 1.2rem;
              color: #a0aec0;
              margin-bottom: 2rem;
              line-height: 1.6;
          }

          .button {
              padding: 1.1rem 2.8rem;
              font-size: 1.2rem;
              font-weight: 600;
              border-radius: 10px;
              border: none;
              cursor: pointer;
              transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
              box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
              text-transform: uppercase;
              letter-spacing: 0.08em;
              margin: 0.5rem;
          }

          .button.login {
              background-color: #63b3ed;
              color: #1a1e27;
          }

          .button.login:hover {
              background-color: #4299e1;
              transform: translateY(-3px) scale(1.02);
          }

          .button.logout {
              background-color: #fc8181;
              color: #1a1e27;
          }

          .button.logout:hover {
              background-color: #e53e3e;
              transform: translateY(-3px) scale(1.02);
          }

          .button.rpc {
              background-color: #68d391;
              color: #1a1e27;
          }

          .button.rpc:hover {
              background-color: #48bb78;
              transform: translateY(-3px) scale(1.02);
          }

          .profile-card {
              background-color: #2d313c;
              border-radius: 15px;
              padding: 2rem;
              margin: 2rem 0;
              text-align: left;
          }

          .profile-picture {
              width: 80px;
              height: 80px;
              border-radius: 50%;
              margin-bottom: 1rem;
              border: 3px solid #63b3ed;
          }

          .status {
              margin: 1rem 0;
              padding: 1rem;
              border-radius: 10px;
              font-weight: 500;
          }

          .status.success {
              background-color: #2d7d32;
              color: #e8f5e8;
          }

          .status.error {
              background-color: #c62828;
              color: #ffebee;
          }

          .status.info {
              background-color: #1976d2;
              color: #e3f2fd;
          }

          .loading {
              display: inline-block;
              width: 20px;
              height: 20px;
              border: 3px solid #f3f3f3;
              border-top: 3px solid #63b3ed;
              border-radius: 50%;
              animation: spin 1s linear infinite;
          }

          @keyframes spin {
              0% { transform: rotate(0deg); }
              100% { transform: rotate(360deg); }
          }

          .hidden {
              display: none;
          }

          pre {
              background-color: #1a1e27;
              padding: 1rem;
              border-radius: 8px;
              overflow-x: auto;
              text-align: left;
              font-size: 0.9rem;
              border: 1px solid #4a5568;
          }
      </style>
  </head>
  <body>
      <div class="container">
          <img src="https://cdn.auth0.com/quantum-assets/dist/latest/logos/auth0/auth0-lockup-en-ondark.png" 
               alt="Auth0 Logo" class="logo" 
               onerror="this.style.display='none'">
          
          <h1>Cap'n Web + Auth0</h1>
          <p class="subtitle">Secure WebSocket RPC with Authentication</p>
          
          <div id="auth-section">
              <button id="login-btn" class="button login">🔐 Login</button>
              <button id="logout-btn" class="button logout hidden">🚪 Logout</button>
          </div>

          <div id="profile-section" class="hidden">
              <div class="profile-card">
                  <div id="profile-info"></div>
              </div>
          </div>

          <div id="rpc-section" class="hidden">
              <h3>🔌 RPC Operations</h3>
              <button id="get-profile-btn" class="button rpc">📋 Get Profile</button>
              <button id="update-profile-btn" class="button rpc">✏️ Update Profile</button>
              <button id="get-public-btn" class="button rpc">🌐 Get Public Data</button>
          </div>

          <div id="status"></div>
          <div id="rpc-results"></div>
      </div>

      <script type="module" src="/app.js"></script>
  </body>
  </html>

6.2: Create the JavaScript client application (client/app.js)
Use capnweb's newWebSocketRpcSession and Auth0 SDK with proper ES module imports:

  import { createAuth0Client } from '@auth0/auth0-spa-js';
  import { newWebSocketRpcSession } from 'capnweb';

  // Configuración de Auth0
  let auth0Client = null;
  let profileApi = null;
  let AUTH0_CONFIG = null;

  // Cargar configuración de Auth0 desde el servidor
  async function loadConfig() {
    const response = await fetch('/api/config');
    const config = await response.json();
    
    AUTH0_CONFIG = {
      domain: config.auth0.domain,
      clientId: config.auth0.clientId,
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: config.auth0.audience,
        scope: 'openid profile email'
      },
      useRefreshTokens: true,
      cacheLocation: 'localstorage'
    };
    
    return AUTH0_CONFIG;
  }

  // Inicializar la aplicación
  async function initializeApp() {
    try {
      showStatus('Loading configuration...', 'info');
      const config = await loadConfig();
      
      showStatus('Initializing Auth0 client...', 'info');
      auth0Client = await createAuth0Client(config);
      
      // Gestionar el callback de redirección
      const query = window.location.search;
      if (query.includes('code=') && query.includes('state=')) {
        showStatus('Processing login...', 'info');
        await auth0Client.handleRedirectCallback();
        window.history.replaceState({}, document.title, window.location.pathname);
      }
      
      // Verificar el estado de autenticación
      const isAuthenticated = await auth0Client.isAuthenticated();
      
      if (isAuthenticated) {
        // Conectar al WebSocket solo si está autenticado
        showStatus('Connecting to Cap\'n Web RPC...', 'info');
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        profileApi = newWebSocketRpcSession(`${protocol}//${window.location.host}/api`);
        
        await showProfileSection();
      } else {
        showAuthSection();
      }
      
      setupEventListeners();
    } catch (error) {
      console.error('Initialization error:', error);
      showStatus(`Failed to initialize: ${error.message}`, 'error');
    }
  }

  // Funciones de autenticación
  async function login() {
    try {
      showStatus('Redirecting to Auth0...', 'info');
      await auth0Client.loginWithRedirect();
    } catch (error) {
      showStatus(`Login failed: ${error.message}`, 'error');
    }
  }

  async function logout() {
    try {
      if (profileApi) {
        profileApi[Symbol.dispose]();
      }
      await auth0Client.logout({
        logoutParams: { returnTo: window.location.origin }
      });
    } catch (error) {
      showStatus(`Logout failed: ${error.message}`, 'error');
    }
  }

  async function getAccessToken() {
    try {
      return await auth0Client.getTokenSilently({
        authorizationParams: {
          audience: AUTH0_CONFIG.authorizationParams.audience
        }
      });
    } catch (error) {
      if (error.error === 'consent_required' || error.error === 'interaction_required') {
        await auth0Client.loginWithRedirect({
          authorizationParams: {
            audience: AUTH0_CONFIG.authorizationParams.audience,
            scope: 'openid profile email',
            prompt: 'consent'
          }
        });
      }
      throw error;
    }
  }

  // Gestión de perfil mediante Cap'n Web RPC
  async function fetchProfile() {
    try {
      showStatus('Fetching profile...', 'info');
      const token = await getAccessToken();
      const user = await auth0Client.getUser();
      
      // Llamar al método RPC directamente en el stub de profileApi
      const profile = await profileApi.getProfile(token);
      
      document.getElementById('userEmail').textContent = user.email || profile.email || 'No email available';
      document.getElementById('bioTextarea').value = profile.bio || '';
      
      showStatus('Profile loaded successfully!', 'success');
    } catch (error) {
      showStatus(`Failed to fetch profile: ${error.message}`, 'error');
    }
  }

  async function saveProfile() {
    try {
      showStatus('Saving profile...', 'info');
      const token = await getAccessToken();
      const bio = document.getElementById('bioTextarea').value;
      
      // Llamar al método RPC directamente en el stub de profileApi
      const result = await profileApi.updateProfile(token, bio);
      
      showStatus(result.message || 'Profile saved successfully!', 'success');
    } catch (error) {
      showStatus(`Failed to save profile: ${error.message}`, 'error');
    }
  }

  // Funciones auxiliares de la interfaz
  function showAuthSection() {
    document.getElementById('authSection').style.display = 'block';
    document.getElementById('profileSection').style.display = 'none';
    showStatus('Ready to login', 'info');
  }

  async function showProfileSection() {
    document.getElementById('authSection').style.display = 'none';
    document.getElementById('profileSection').style.display = 'block';
    await fetchProfile();
  }

  function showStatus(message, type) {
    const statusEl = document.getElementById('status');
    statusEl.textContent = message;
    statusEl.className = `status ${type}`;
  }

  // Escuchadores de eventos
  function setupEventListeners() {
    document.getElementById('loginBtn').addEventListener('click', login);
    document.getElementById('logoutBtn').addEventListener('click', logout);
    document.getElementById('fetchBtn').addEventListener('click', fetchProfile);
    document.getElementById('saveBtn').addEventListener('click', saveProfile);
  }

  // Inicializar la aplicación cuando el DOM esté cargado
  document.addEventListener('DOMContentLoaded', initializeApp);
        
        if (this.isAuthenticated) {
          this.user = await this.auth0Client.getUser();
          this.accessToken = await this.auth0Client.getTokenSilently();
          this.showLoggedInState();
          this.connectWebSocket();
        } else {
          this.showLoggedOutState();
        }

        this.setupEventListeners();
        this.showStatus('✅ Application initialized successfully', 'success');
      } catch (error) {
        console.error('❌ Initialization failed:', error);
        this.showStatus(`❌ Initialization failed: ${error.message}`, 'error');
      }
    }

    setupEventListeners() {
      document.getElementById('login-btn').addEventListener('click', () => this.login());
      document.getElementById('logout-btn').addEventListener('click', () => this.logout());
      document.getElementById('get-profile-btn').addEventListener('click', () => this.getProfile());
      document.getElementById('update-profile-btn').addEventListener('click', () => this.updateProfile());
      document.getElementById('get-public-btn').addEventListener('click', () => this.getPublicData());
    }

    async login() {
      try {
        this.showStatus('🔄 Redirecting to Auth0...', 'info');
        await this.auth0Client.loginWithRedirect();
      } catch (error) {
        console.error('❌ Login failed:', error);
        this.showStatus(`❌ Login failed: ${error.message}`, 'error');
      }
    }

    async logout() {
      try {
        this.closeWebSocket();
        await this.auth0Client.logout({
          logoutParams: {
            returnTo: window.location.origin
          }
        });
      } catch (error) {
        console.error('❌ Logout failed:', error);
        this.showStatus(`❌ Logout failed: ${error.message}`, 'error');
      }
    }

    connectWebSocket() {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        return; // Ya conectado
      }

      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      const wsUrl = `${protocol}//${window.location.host}`;
      
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('🔌 WebSocket conectado');
        this.showStatus('🔌 Connected to Cap\'n Web server', 'success');
      };

      this.ws.onmessage = (event) => {
        try {
          const response = JSON.parse(event.data);
          const pendingRequest = this.pendingRequests.get(response.id);
          
          if (pendingRequest) {
            this.pendingRequests.delete(response.id);
            if (response.error) {
              pendingRequest.reject(new Error(response.error));
            } else {
              pendingRequest.resolve(response.result);
            }
          }
        } catch (error) {
          console.error('❌ Error al analizar el mensaje de WebSocket:', error);
        }
      };

      this.ws.onerror = (error) => {
        console.error('❌ WebSocket error:', error);
        this.showStatus('❌ WebSocket connection error', 'error');
      };

      this.ws.onclose = () => {
        console.log('🔌 WebSocket desconectado');
        this.showStatus('🔌 Disconnected from server', 'info');
        
        // Reintentar la conexión después de 3 segundos si está autenticado
        if (this.isAuthenticated) {
          setTimeout(() => this.connectWebSocket(), 3000);
        }
      };
    }

    closeWebSocket() {
      if (this.ws) {
        this.ws.close();
        this.ws = null;
      }
    }

    async callRPC(method, params = null, requiresAuth = true) {
      return new Promise((resolve, reject) => {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
          return reject(new Error('WebSocket not connected'));
        }

        const id = ++this.requestId;
        const request = {
          id,
          method,
          params
        };

        if (requiresAuth && this.accessToken) {
          request.token = this.accessToken;
        }

        this.pendingRequests.set(id, { resolve, reject });
        
        // Establecer tiempo de espera para la solicitud
        setTimeout(() => {
          if (this.pendingRequests.has(id)) {
            this.pendingRequests.delete(id);
            reject(new Error('RPC request timeout'));
          }
        }, 10000);

        this.ws.send(JSON.stringify(request));
      });
    }

    async getProfile() {
      try {
        this.showStatus('🔄 Fetching profile...', 'info');
        const profile = await this.callRPC('getProfile');
        this.showRPCResult('Profile Data', profile);
      } catch (error) {
        console.error('❌ Get profile failed:', error);
        this.showStatus(`❌ Failed to get profile: ${error.message}`, 'error');
      }
    }

    async updateProfile() {
      try {
        this.showStatus('🔄 Updating profile...', 'info');
        const updates = {
          preferences: {
            theme: 'dark',
            notifications: true,
            lastAction: 'profile-update'
          }
        };
        
        const updatedProfile = await this.callRPC('updateProfile', updates);
        this.showRPCResult('Updated Profile', updatedProfile);
      } catch (error) {
        console.error('❌ Update profile failed:', error);
        this.showStatus(`❌ Failed to update profile: ${error.message}`, 'error');
      }
    }

    async getPublicData() {
      try {
        this.showStatus('🔄 Fetching public data...', 'info');
        const data = await this.callRPC('getPublicData', null, false);
        this.showRPCResult('Public Data', data);
      } catch (error) {
        console.error('❌ Get public data failed:', error);
        this.showStatus(`❌ Failed to get public data: ${error.message}`, 'error');
      }
    }

    showLoggedInState() {
      document.getElementById('login-btn').classList.add('hidden');
      document.getElementById('logout-btn').classList.remove('hidden');
      document.getElementById('profile-section').classList.remove('hidden');
      document.getElementById('rpc-section').classList.remove('hidden');

      if (this.user) {
        const placeholderImage = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%2363b3ed'/%3E%3Cpath d='M50 45c7.5 0 13.64-6.14 13.64-13.64S57.5 17.72 50 17.72s-13.64 6.14-13.64 13.64S42.5 45 50 45zm0 6.82c-9.09 0-27.28 4.56-27.28 13.64v3.41c0 1.88 1.53 3.41 3.41 3.41h47.74c1.88 0 3.41-1.53 3.41-3.41v-3.41c0-9.08-18.19-13.64-27.28-13.64z' fill='%23fff'/%3E%3C/svg%3E`;
        const profileHtml = `
          <img src="${this.user.picture || placeholderImage}" alt="Profile" class="profile-picture" onerror="this.src='${placeholderImage}'">
          <h3>${this.user.name || 'User'}</h3>
          <p><strong>Email:</strong> ${this.user.email || 'Not provided'}</p>
          <p><strong>User ID:</strong> ${this.user.sub}</p>
        `;
        document.getElementById('profile-info').innerHTML = profileHtml;
      }
    }

    showLoggedOutState() {
      document.getElementById('login-btn').classList.remove('hidden');
      document.getElementById('logout-btn').classList.add('hidden');
      document.getElementById('profile-section').classList.add('hidden');
      document.getElementById('rpc-section').classList.add('hidden');
    }

    showStatus(message, type = 'info') {
      const statusDiv = document.getElementById('status');
      statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
      
      // Ocultar automáticamente los mensajes de éxito e información después de 5 segundos
      if (type === 'success' || type === 'info') {
        setTimeout(() => {
          statusDiv.innerHTML = '';
        }, 5000);
      }
    }

    showRPCResult(title, data) {
      const resultsDiv = document.getElementById('rpc-results');
      const resultHtml = `
        <div class="status success">
          <h4>${title}</h4>
          <pre>${JSON.stringify(data, null, 2)}</pre>
        </div>
      `;
      resultsDiv.innerHTML = resultHtml;
      this.showStatus('✅ RPC call completed successfully', 'success');
    }
  }

  // Inicializar la aplicación
  const app = new CapnWebAuth0Client();
  app.init().catch(error => {
  // Inicializar la aplicación cuando el DOM esté cargado
  document.addEventListener('DOMContentLoaded', initializeApp);

Step 7: Test and Run the Application
7.1: Start the development server:

  npm run start

7.2: Open http://localhost:3000 in your browser

7.3: Test the complete authentication flow:
  - Click "Login" to authenticate with Auth0
  - View your profile information (email will be displayed from Auth0)
  - Update your bio and save
  - Refresh the page - you should remain logged in (thanks to refresh tokens)
  - Test logout functionality
  - Test RPC calls (Get Profile, Update Profile, Get Public Data)
  - Verify WebSocket connection status
  - Test logout functionality

SECURITY REQUIREMENTS & BEST PRACTICES
- ✅ NEVER accept unauthenticated RPC calls for protected methods
- ✅ ALWAYS validate JWT signatures using JWKS from Auth0
- ✅ Implement comprehensive error handling for expired/invalid tokens
- ✅ Use environment variables for all sensitive configuration
- ✅ Validate all user inputs before processing
- ✅ Log security events and authentication attempts
- ✅ Use secure WebSocket connections (WSS) in production
- ✅ Implement proper CORS policies
- ✅ Add request rate limiting for production use
- ✅ Sanitize all data before storage or transmission

TROUBLESHOOTING TIPS
- Check browser console for JavaScript errors
- Verify .env file contains correct Auth0 configuration
- Ensure Auth0 application settings match your local URLs
- Confirm API scopes are properly configured in Auth0 dashboard
- Test WebSocket connectivity separately if RPC calls fail
- Validate JWT tokens using jwt.io for debugging

Primeros pasos

Esta guía de inicio rápido muestra cómo añadir la autenticación de Auth0 a una aplicación web de Cap’n Web. Crearás una aplicación web moderna basada en RPC con funcionalidad de inicio de sesión segura mediante el framework de JavaScript de Cap’n Web y el SDK de Auth0 para SPA.
1

Crear un proyecto nuevo

Crea un proyecto nuevo de Cap’n Web y configura la estructura básica:
mkdir capnweb-auth0-app && cd capnweb-auth0-app
Inicializa el proyecto y configúralo para usar módulos ES:
npm init -y && npm pkg set type="module"
Cree la estructura de carpetas del proyecto:
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
2

Instalar dependencias

Instala Cap’n Web y las dependencias básicas:
npm install capnweb ws dotenv
Instala los SDK de Auth0 para la autenticación y la validación de tokens:
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js se usa en el cliente para gestionar la autenticación de usuarios, los flujos de inicio de sesión y la gestión de tokens en el navegador.@auth0/auth0-api-js se usa en el servidor para verificar tokens de acceso y validar firmas JWT mediante el JWKS de Auth0.
Configura el script de inicio en package.json:
npm pkg set scripts.start="node server/index.js"
3

Configura tu aplicación de Auth0

A continuación, debes crear una nueva aplicación en tu tenant de Auth0 y agregar las variables de entorno a tu proyecto.Puede hacerlo automáticamente ejecutando un comando de CLI o de forma manual desde el Dashboard:
Ejecuta el siguiente comando de shell en el directorio raíz de tu proyecto para crear una aplicación de Auth0 y generar el archivo .env:
5

Crear el servidor

Crea el servidor web Cap’n con integración de Auth0:
server/index.js
import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';

dotenv.config();

const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();

// Configuración de Auth0
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
const PORT = process.env.PORT || 3000;

if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
  console.error('❌ Missing required Auth0 environment variables');
  if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN is required');
  if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID is required');
  if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE is required');
  process.exit(1);
}

// Inicializar el cliente de API de Auth0 para verificación de tokens
const auth0ApiClient = new ApiClient({
  domain: AUTH0_DOMAIN,
  audience: AUTH0_AUDIENCE
});

async function verifyToken(token) {
  try {
    const payload = await auth0ApiClient.verifyAccessToken({
      accessToken: token
    });
    return payload;
  } catch (error) {
    throw new Error(`Token verification failed: ${error.message}`);
  }
}

// Servicio de perfil - Destino RPC de Cap'n Web
class ProfileService extends RpcTarget {
  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    const userId = decoded.sub;
    const profile = userProfiles.get(userId) || { bio: '' };
    
    return {
      id: userId,
      email: decoded.email || 'Unknown User',
      bio: profile.bio
    };
  }

  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    const userId = decoded.sub;
    userProfiles.set(userId, { bio });
    
    return { success: true, message: 'Profile updated successfully' };
  }
}

// Crear servidor HTTP
const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  if (req.url === '/api/config') {
    const config = { 
      auth0: { 
        domain: AUTH0_DOMAIN, 
        clientId: AUTH0_CLIENT_ID,
        audience: AUTH0_AUDIENCE
      } 
    };
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    res.end(JSON.stringify(config));
    return;
  }

  // Gestionar la ruta raíz y el callback de Auth0
  if (req.url === '/' || req.url === '/index.html' || req.url.startsWith('/?code=') || req.url.startsWith('/?error=')) {
    const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
    res.setHeader('Content-Type', 'text/html');
    res.writeHead(200);
    res.end(html);
    return;
  }

  if (req.url === '/app.js') {
    const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  // Servir el SDK JS de Auth0 SPA desde node_modules
  if (req.url === '/@auth0/auth0-spa-js') {
    const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js');
    const js = readFileSync(modulePath, 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  // Servir capnweb desde node_modules
  if (req.url === '/capnweb') {
    const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js');
    const js = readFileSync(modulePath, 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

// Servidor WebSocket para RPC de Cap'n Web
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
  // Solo gestionar conexiones RPC en la ruta /api
  if (req.url === '/api') {
    console.log('🔗 New Cap\'n Web RPC connection');
    
    // Crear una nueva instancia de ProfileService para esta conexión
    const profileService = new ProfileService();
    
    // Usar newWebSocketRpcSession de capnweb para gestionar la conexión
    newWebSocketRpcSession(ws, profileService);
  }
});

// Iniciar servidor
server.listen(PORT, () => {
  console.log(`🚀 Cap'n Web Auth0 Server Started`);
  console.log(`📍 Server running on http://localhost:${PORT}`);
  console.log(`🔐 Auth0 Domain: ${AUTH0_DOMAIN}`);
  console.log(`🆔 Client ID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
  console.log(`🎯 API Audience: ${AUTH0_AUDIENCE}`);
});
6

Crear la interfaz del cliente

Crea los archivos HTML y JavaScript del frontend:
7

Ejecuta tu aplicación

npm run start
Punto de controlAhora deberías tener una página de inicio de sesión de Auth0 totalmente funcional ejecutándose en tu localhost

Uso avanzado

Mejora la seguridad añadiendo validaciones adicionales y limitación de velocidad a tus métodos RPC:
server/profile-service.js
import rateLimit from 'express-rate-limit';

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // Limitación de velocidad simple en memoria
  }

  async validateAndRateLimit(userId) {
    const now = Date.now();
    const userLimit = this.rateLimiter.get(userId) || { count: 0, resetTime: now + 60000 };
    
    if (now > userLimit.resetTime) {
      userLimit.count = 0;
      userLimit.resetTime = now + 60000;
    }
    
    if (userLimit.count >= 10) {
      throw new Error('Rate limit exceeded. Try again later.');
    }
    
    userLimit.count++;
    this.rateLimiter.set(userId, userLimit);
  }

  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    await this.validateAndRateLimit(decoded.sub);
    
    const userId = decoded.sub;
    const profile = userProfiles.get(userId) || { bio: '' };
    
    return {
      id: userId,
      email: decoded.email || 'Unknown User',
      bio: profile.bio,
      lastUpdated: profile.lastUpdated || null
    };
  }
}
Implementa un manejo adecuado de la conexión WebSocket con reconexión automática:
client/connection-manager.js
import { newWebSocketRpcSession } from 'capnweb';

class RpcConnectionManager {
  constructor(wsUrl, options = {}) {
    this.wsUrl = wsUrl;
    this.api = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.isConnecting = false;
    this.onReconnect = options.onReconnect || (() => {});
  }

  async connect() {
    if (this.isConnecting) return this.api;
    this.isConnecting = true;

    try {
      // Libera la conexión existente, si la hay
      if (this.api) {
        this.api[Symbol.dispose]();
      }

      // Crea una nueva sesión RPC de WebSocket
      this.api = newWebSocketRpcSession(this.wsUrl);
      this.reconnectAttempts = 0;
      this.isConnecting = false;
      
      console.log('✅ RPC connection established');
      this.onReconnect(this.api);
      
      return this.api;
    } catch (error) {
      this.isConnecting = false;
      
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        console.log(`🔄 Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
        
        await new Promise(resolve => 
          setTimeout(resolve, this.reconnectDelay * this.reconnectAttempts)
        );
        
        return this.connect();
      } else {
        throw new Error('Max reconnection attempts reached');
      }
    }
  }

  disconnect() {
    if (this.api) {
      this.api[Symbol.dispose]();
      this.api = null;
    }
    this.reconnectAttempts = 0;
  }

  getApi() {
    return this.api;
  }
}

// Uso en app.js
const connectionManager = new RpcConnectionManager(
  `${protocol}//${host}/api`,
  {
    maxReconnectAttempts: 5,
    reconnectDelay: 1000,
    onReconnect: async (api) => {
      // Actualiza la UI o vuelve a cargar los datos después de la reconexión
      await displayProfile(api);
    }
  }
);

// Conectarse al autenticarse
if (isAuthenticated) {
  await connectionManager.connect();
  profileApi = connectionManager.getApi();
}
Sustituya el almacenamiento en memoria por una base de datos para entornos de producción:
server/database.js
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgresql://localhost/capnweb_auth0'
});

// Inicializa el esquema de la base de datos
async function initializeDatabase() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS user_profiles (
      user_id VARCHAR(255) PRIMARY KEY,
      bio TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

class DatabaseProfileService extends RpcTarget {
  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    const result = await pool.query(
      'SELECT bio, updated_at FROM user_profiles WHERE user_id = $1',
      [decoded.sub]
    );
    
    return {
      id: decoded.sub,
      email: decoded.email,
      bio: result.rows[0]?.bio || '',
      lastUpdated: result.rows[0]?.updated_at || null
    };
  }
  
  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    await pool.query(`
      INSERT INTO user_profiles (user_id, bio, updated_at) 
      VALUES ($1, $2, CURRENT_TIMESTAMP)
      ON CONFLICT (user_id) 
      DO UPDATE SET bio = $2, updated_at = CURRENT_TIMESTAMP
    `, [decoded.sub, bio]);
    
    return { success: true, message: 'Perfil actualizado correctamente' };
  }
}

export { initializeDatabase, DatabaseProfileService };