> ## Documentation Index
> Fetch the complete documentation index at: https://translations.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Esta guía muestra cómo integrar Auth0, añadir autenticación y mostrar información del perfil del usuario en una aplicación de página única (SPA) que usa Cap'n Web RPC mediante el SDK de Auth0 para SPA.

# Añade el inicio de sesión a tu aplicación de Cap'n Web

export const HowToSchema = () => <script type="application/ld+json">
    {'{"@context":"https://schema.org","@type":"HowTo"}'}
  </script>;

export const AuthCodeGroup = ({children, dropdown}) => {
  const [processedChildren, setProcessedChildren] = useState(children);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      unsubscribe = window.autorun(() => {
        const processChildren = node => {
          if (typeof node === "string") {
            let processedNode = node;
            for (const [key, value] of window.rootStore.variableStore.values.entries()) {
              const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
              processedNode = processedNode.replaceAll(new RegExp(escapedKey, "g"), value);
            }
            return processedNode;
          } else if (Array.isArray(node)) {
            return node.map(processChildren);
          } else if (node && node.props && node.props.children) {
            return {
              ...node,
              props: {
                ...node.props,
                children: processChildren(node.props.children)
              }
            };
          }
          return node;
        };
        setProcessedChildren(processChildren(children));
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  return <CodeGroup dropdown={dropdown}>{processedChildren}</CodeGroup>;
};

<HowToSchema />

<Callout icon="pencil" color="#FFC107" iconType="solid">
  Esta guía de inicio rápido está actualmente en **Beta**. ¡Nos encantaría recibir tus comentarios!
</Callout>

<Accordion title="Prompt de IA" defaultOpen icon="microchip-ai" iconType="sharp-solid">
  **¿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.

  ```markdown expandable theme={null}
  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
  ```
</Accordion>

<div id="get-started">
  ## Primeros pasos
</div>

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.

<Steps>
  <Step title="Crear un proyecto nuevo" stepNumber={1}>
    Crea un proyecto nuevo de Cap'n Web y configura la estructura básica:

    ```shellscript theme={null}
    mkdir capnweb-auth0-app && cd capnweb-auth0-app
    ```

    Inicializa el proyecto y configúralo para usar módulos ES:

    ```shellscript theme={null}
    npm init -y && npm pkg set type="module"
    ```

    Cree la estructura de carpetas del proyecto:

    ```shellscript theme={null}
    mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
    ```
  </Step>

  <Step title="Instalar dependencias" stepNumber={2}>
    Instala Cap'n Web y las dependencias básicas:

    ```shellscript theme={null}
    npm install capnweb ws dotenv
    ```

    Instala los SDK de Auth0 para la autenticación y la validación de tokens:

    ```shellscript theme={null}
    npm install @auth0/auth0-spa-js @auth0/auth0-api-js
    ```

    <Info>
      **@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.
    </Info>

    Configura el script de inicio en package.json:

    ```shellscript theme={null}
    npm pkg set scripts.start="node server/index.js"
    ```
  </Step>

  <Step title="Configura tu aplicación de Auth0" stepNumber={3}>
    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:

    <Tabs>
      <Tab title="CLI">
        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`:

        <AuthCodeGroup>
          ```shellscript Mac theme={null}
          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
          ```

          ```shellscript Windows theme={null}
          $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
          ```
        </AuthCodeGroup>
      </Tab>

      <Tab title="Panel">
        Antes de empezar, crea un archivo `.env` en el directorio raíz del proyecto

        ```shellscript .env theme={null}
        AUTH0_DOMAIN=YOUR_AUTH0_APP_DOMAIN
        AUTH0_CLIENT_ID=YOUR_AUTH0_APP_CLIENT_ID
        AUTH0_AUDIENCE=https://capnweb-api.yourproject.com
        PORT=3000
        NODE_ENV=development
        ```

        1. Ve al [Auth0 Dashboard](https://manage.auth0.com/dashboard/)
        2. Haz clic en **Applications** > **Applications** > **Create Application**
        3. En la ventana emergente, introduce un nombre para tu aplicación, selecciona `Single Page Web Application` como tipo de aplicación y haz clic en **Create**
        4. Ve a la pestaña **Settings** en la página de detalles de la aplicación
        5. Reemplaza `YOUR_AUTH0_APP_DOMAIN` y `YOUR_AUTH0_APP_CLIENT_ID` en el archivo `.env` por los valores de **dominio** e **ID de cliente** del dashboard

        Por último, en la pestaña **Settings** de la página de detalles de la aplicación, configura las siguientes URL:

        **Allowed Callback URLs:**

        ```
        http://localhost:3000
        ```

        **URL de cierre de sesión permitidas:**

        ```
        http://localhost:3000
        ```

        **Orígenes web permitidos:**

        ```
        http://localhost:3000
        ```

        <Info>
          **Allowed Callback URLs** son una medida de seguridad fundamental para garantizar que los usuarios vuelvan de forma segura a tu aplicación después de autenticarse. Sin una URL que coincida, el proceso de inicio de sesión fallará y los usuarios verán una página de error de Auth0 en lugar de acceder a tu aplicación.

          **Allowed Logout URLs** son esenciales para ofrecer una experiencia fluida al cerrar sesión. Sin una URL que coincida, los usuarios no volverán a tu aplicación después de cerrar sesión, sino que permanecerán en una página genérica de Auth0.

          **Allowed Web Origins** es fundamental para la autenticación silenciosa. Sin esta configuración, los usuarios cerrarán sesión al actualizar la página o al volver más tarde a tu aplicación.
        </Info>

        **Obligatorio: crea una API de Auth0**

        Debes crear una API de Auth0 para que la validación de tokens funcione:

        1. En el Auth0 Dashboard, ve a **APIs** > **Create API**
        2. Configura estos valores:
           * **Nombre**: `Cap'n Web API`
           * **Identifier**: `https://capnweb-api.yourproject.com` (usa tu propio identificador único)
           * **Signing Algorithm**: `RS256` (predeterminado)
        3. Haz clic en **Create**
        4. En la pestaña **Scopes**, agrega estos alcances:
           * `read:profile` - Leer los datos del perfil del usuario
           * `write:profile` - Actualizar los datos del perfil del usuario
        5. Actualiza tu archivo `.env` con el identificador de la API como `AUTH0_AUDIENCE`

        <Warning>
          Si no creas una API de Auth0, recibirás un error "access\_denied" durante el inicio de sesión. El identificador de la API debe coincidir exactamente con tu variable de entorno `AUTH0_AUDIENCE`.
        </Warning>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Crear el servidor" stepNumber={5}>
    Crea el servidor web Cap'n con integración de Auth0:

    ```javascript server/index.js expandable lines theme={null}
    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}`);
    });
    ```
  </Step>

  <Step title="Crear la interfaz del cliente" stepNumber={6}>
    Crea los archivos HTML y JavaScript del frontend:

    <AuthCodeGroup>
      ```html client/index.html expandable lines theme={null}
      <!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>
          <style>
              body {
                  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                  max-width: 600px;
                  margin: 0 auto;
                  padding: 2rem;
                  line-height: 1.6;
              }
              .auth-section, .profile-section {
                  background: #f8f9fa;
                  border-radius: 8px;
                  padding: 2rem;
                  margin: 1rem 0;
              }
              .profile-section { display: none; }
              button {
                  background: #007bff;
                  color: white;
                  border: none;
                  padding: 0.75rem 1.5rem;
                  border-radius: 4px;
                  cursor: pointer;
                  font-size: 1rem;
                  margin: 0.5rem 0.5rem 0.5rem 0;
              }
              button:hover { background: #0056b3; }
              button.secondary { background: #6c757d; }
              button.secondary:hover { background: #545b62; }
              textarea {
                  width: 100%;
                  min-height: 100px;
                  margin: 1rem 0;
                  padding: 0.75rem;
                  border: 1px solid #ddd;
                  border-radius: 4px;
                  resize: vertical;
              }
              .status {
                  padding: 1rem;
                  margin: 1rem 0;
                  border-radius: 4px;
                  font-weight: 500;
              }
              .status.success { background: #d4edda; color: #155724; }
              .status.error { background: #f8d7da; color: #721c24; }
              .status.info { background: #d1ecf1; color: #0c5460; }
          </style>
      </head>
      <body>
          <h1>Cap'n Web + Auth0 Profile Demo</h1>
          
          <div id="status" class="status info">Initializing...</div>
          
          <!-- Sección de inicio de sesión -->
          <div id="authSection" class="auth-section">
              <h2>🔐 Welcome!</h2>
              <p>This demo shows Auth0 authentication with Cap'n Web RPC for real-time profile management.</p>
              <button id="loginBtn">Login with Auth0</button>
          </div>
          
          <!-- Sección de perfil -->
          <div id="profileSection" class="profile-section">
              <h2>👤 Profile Management</h2>
              <p><strong>Email:</strong> <span id="userEmail">Loading...</span></p>
              
              <label for="bioTextarea"><strong>Bio:</strong></label>
              <textarea id="bioTextarea" placeholder="Tell us about yourself..."></textarea>
              
              <div>
                  <button id="fetchBtn">🔄 Fetch Profile</button>
                  <button id="saveBtn">💾 Save Profile</button>
                  <button id="logoutBtn" class="secondary">🚪 Logout</button>
              </div>
          </div>

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

      ```javascript client/app.js expandable lines theme={null}
      import { createAuth0Client } from '@auth0/auth0-spa-js';
      import { newWebSocketRpcSession } from 'capnweb';

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

      // Configuración de Auth0 - Cargada dinámicamente desde el servidor
      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);
          
          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);
          }
          
          const isAuthenticated = await auth0Client.isAuthenticated();
          
          if (isAuthenticated) {
            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;
        }
      }

      // Funciones de gestión de perfil
      async function fetchProfile() {
        try {
          showStatus('Fetching profile...', 'info');
          
          const token = await getAccessToken();
          const user = await auth0Client.getUser();
          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;
          
          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 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}`;
      }

      // Listeners 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);
      ```
    </AuthCodeGroup>
  </Step>

  <Step title="Ejecuta tu aplicación" stepNumber={7}>
    ```shellscript theme={null}
    npm run start
    ```
  </Step>
</Steps>

<Check>
  **Punto de control**

  Ahora deberías tener una página de inicio de sesión de Auth0 totalmente funcional ejecutándose en tu [localhost](http://localhost:3000/)
</Check>

***

<div id="advanced-usage">
  ## Uso avanzado
</div>

<Accordion title="Seguridad de RPC en el servidor">
  Mejora la seguridad añadiendo validaciones adicionales y limitación de velocidad a tus métodos RPC:

  ```javascript server/profile-service.js theme={null}
  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
      };
    }
  }
  ```
</Accordion>

<Accordion title="Gestión de conexiones WebSocket">
  Implementa un manejo adecuado de la conexión WebSocket con reconexión automática:

  ```javascript client/connection-manager.js theme={null}
  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();
  }
  ```
</Accordion>

<Accordion title="Integración con base de datos">
  Sustituya el almacenamiento en memoria por una base de datos para entornos de producción:

  ```javascript server/database.js theme={null}
  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 };
  ```
</Accordion>
