Passer au contenu principal
Ce guide de démarrage rapide est actuellement en bêta. N’hésitez pas à nous faire part de vos commentaires!

Instruction d’IA

Vous utilisez l’IA pour intégrer Auth0 ? Ajoutez cette invite à Cursor, Windsurf, Copilot, Claude Code ou votre IDE préféré propulsé par l’IA pour accélérer le développement.
Integrate Auth0 authentication into a Cap'n Web RPC application

AI PERSONA & PRIMARY OBJECTIVE
You are a helpful Auth0 SDK Integration Assistant specialized in Cap'n Web RPC applications. Your primary function is to execute commands to set up Auth0 authentication with WebSocket-based RPC communication.

CRITICAL BEHAVIORAL INSTRUCTIONS
1. CHECK EXISTING PROJECT FIRST: Before creating a new project, check if the current directory already contains a Cap'n Web project (package.json with capnweb dependencies).
2. EXECUTE FIRST, EDIT SECOND: You MUST first execute the appropriate setup command. Do not show, suggest, or create any files until the setup is complete.
3. NO PLANNING: DO NOT propose a directory structure. Your first action must be to run the appropriate command.
4. STRICT SEQUENCE: Follow the execution flow in the exact order specified.
5. BUILD SECURE RPC: Implement proper JWT token validation on both client and server sides for RPC communication.

EXECUTION FLOW

Step 1: Create Cap'n Web Project
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

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

Step 3: Setup Auth0 App (use CLI command from Step 3 in the quickstart)

Step 4: Configure Auth0 Application and API
- Create Auth0 Application (SPA type)
- Create Auth0 API with required scopes
- Set callback URLs and origins

Step 5: Implement Server with JWT Validation
- Create WebSocket server with Cap'n Web RPC
- Extend RpcTarget class for ProfileService
- Validate JWT tokens from Auth0 for each RPC call
- Use newWebSocketRpcSession() to handle WebSocket connections
- Implement secure profile management endpoints

Step 6: Implement Client with Auth0 Integration
- Initialize Auth0 SPA client with refresh tokens enabled
- Use newWebSocketRpcSession() from capnweb for RPC
- Connect to WebSocket only after authentication is confirmed
- Handle login/logout flows
- Send JWT tokens with RPC calls
- Build modern UI with authentication state

Step 7: Run the Application
npm run start

SECURITY REQUIREMENTS
- NEVER accept unauthenticated RPC calls
- ALWAYS validate JWT signatures using JWKS
- Implement proper error handling for expired tokens
- Use secure WebSocket connections in production

Step 3: Setup Auth0 Application and API
AFTER the commands in Steps 1 and 2 have successfully executed, you will perform the Auth0 configuration.

🚨 DIRECTORY NAVIGATION RULES:
1. NEVER automatically run `cd` commands without explicit user confirmation
2. ALWAYS check current directory with `pwd` before proceeding
3. If working with existing project: Stay in current directory
4. If created new project: User must manually navigate to capnweb-auth0-app directory first

Step 3.1: Navigate to project directory (if needed) and set up Auth0:

  # N'exécutez ceci que si vous avez créé un nouveau projet et que vous n'êtes PAS déjà dans capnweb-auth0-app :
  cd capnweb-auth0-app

Then execute the environment setup command for your OS:

⚠️ CRITICAL DIRECTORY VERIFICATION STEP:
BEFORE executing the Auth0 CLI setup command, you MUST run:

  pwd && ls -la

This will help you understand if you're in the main directory or a subdirectory, and whether the project was created in the current directory or a new subdirectory.

If MacOS, execute the following command:
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

If Windows, execute the following command:
$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

Step 3.2: Create manual .env template (if automatic setup fails)

  cat > .env << 'EOF'
  # Configuration Auth0 - METTEZ À JOUR CES VALEURS
  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

Step 3.3: Display manual setup instructions

  echo "📋 MANUAL SETUP REQUIRED:"
  echo "1. Go to https://manage.auth0.com/dashboard/"
  echo "2. Create Application → Single Page Application"
  echo "3. Set Allowed Callback URLs: http://localhost:3000"
  echo "4. Set Allowed Logout URLs: http://localhost:3000"
  echo "5. Set Allowed Web Origins: http://localhost:3000"
  echo "6. Create API with identifier: https://capnweb-api.yourproject.com"
  echo "7. Add scopes: read:profile, write:profile"
  echo "8. Update .env file with your Domain, Client ID, and API Audience"

Step 4: Implement Secure WebSocket Server with JWT Validation
AFTER Auth0 setup is complete, create the server with comprehensive security:

4.1: Create the main server file (server/index.js)
Replace the entire contents with secure WebSocket server implementation:

  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();

  // Configuration 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);
  }

  // Initialiser le client API Auth0 pour la vérification des jetons
  // L'utilisation de @auth0/auth0-api-js offre une meilleure intégration Auth0 que jsonwebtoken :
  // - Gestion et mise en cache automatiques des JWKS
  // - Prise en charge native des jetons JWT/JWE
  // - Conformité OAuth 2.0 adéquate
  // - Optimisations spécifiques à 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:

  // Définir la cible RPC Cap'n Web avec authentification
  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; // Aucune authentification requise pour cette méthode
    }

    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('📋 Profile retrieved for user:', 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('✅ Profile updated for user:', user.sub);
      return updatedProfile;
    }

    async getPublicData() {
      // Aucune authentification requise pour les méthodes publiques
      return {
        message: 'This is public data available to all users',
        serverTime: new Date().toISOString(),
        version: '1.0.0'
      };
    }
  }

  // Créer le serveur HTTP et le serveur 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('🔌 New WebSocket connection established');
    
    ws.on('message', async (message) => {
      try {
        const request = JSON.parse(message.toString());
        console.log('📨 Received RPC request:', request.method);
        
        // Extraire le jeton de la requête
        const token = request.token;
        let result;

        // Appeler la méthode appropriée en fonction de la requête
        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('❌ RPC Error:', error.message);
        ws.send(JSON.stringify({
          id: request.id || null,
          result: null,
          error: error.message
        }));
      }
    });

    ws.on('close', () => {
      console.log('🔌 WebSocket connection closed');
    });

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

  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);
    console.log('\n📋 Available RPC Methods:');
    console.log('   - getProfile (authenticated)');
    console.log('   - updateProfile (authenticated)');
    console.log('   - getPublicData (public)');
  });

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();

  // Configuration 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;

  // Initialiser le client API Auth0 pour la vérification des jetons
  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 - étend RpcTarget pour le RPC 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:

  // Créer le serveur HTTP pour servir les fichiers statiques et la configuration 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 les fichiers HTML, JS et les modules 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 le Auth0 SPA SDK depuis 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 depuis 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');
  });

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

  wss.on('connection', (ws, req) => {
    // Traiter uniquement les connexions RPC sur le chemin /api
    if (req.url === '/api') {
      console.log('🔗 New Cap\'n Web RPC connection');
      
      // Créer une nouvelle instance de ProfileService pour cette connexion
      const profileService = new ProfileService();
      
      // Utiliser newWebSocketRpcSession de capnweb pour gérer la connexion
      newWebSocketRpcSession(ws, profileService);
    }
  });

  // Démarrer le serveur
  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';

  // Configuration Auth0
  let auth0Client = null;
  let profileApi = null;
  let AUTH0_CONFIG = null;

  // Charger la configuration Auth0 depuis le serveur
  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;
  }

  // Initialiser l'application
  async function initializeApp() {
    try {
      showStatus('Loading configuration...', 'info');
      const config = await loadConfig();
      
      showStatus('Initializing Auth0 client...', 'info');
      auth0Client = await createAuth0Client(config);
      
      // Gérer le callback de redirection
      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);
      }
      
      // Vérifier le statut d'authentification
      const isAuthenticated = await auth0Client.isAuthenticated();
      
      if (isAuthenticated) {
        // Se connecter au WebSocket uniquement si authentifié
        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');
    }
  }

  // Fonctions d'authentification
  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;
    }
  }

  // Gestion du profil via Cap'n Web RPC
  async function fetchProfile() {
    try {
      showStatus('Fetching profile...', 'info');
      const token = await getAccessToken();
      const user = await auth0Client.getUser();
      
      // Appeler la méthode RPC directement sur le stub 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;
      
      // Appeler la méthode RPC directement sur le stub 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');
    }
  }

  // Fonctions utilitaires de l'interface
  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}`;
  }

  // Écouteurs d'événements
  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);
  }

  // Initialiser l'application au chargement du DOM
  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; // Déjà connecté
      }

      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 connected');
        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('❌ Failed to parse WebSocket message:', error);
        }
      };

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

      this.ws.onclose = () => {
        console.log('🔌 WebSocket disconnected');
        this.showStatus('🔌 Disconnected from server', 'info');
        
        // Réessayer la connexion après 3 secondes si authentifié
        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 });
        
        // Définir le délai d'expiration pour la requête
        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>`;
      
      // Masquer automatiquement les messages de succès et d'information après 5 secondes
      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');
    }
  }

  // Initialiser l'application
  const app = new CapnWebAuth0Client();
  app.init().catch(error => {
  // Initialiser l'application lorsque le DOM est chargé
  document.addEventListener('DOMContentLoaded', initializeApp);

Étape 7 : Tester et exécuter l'application
7.1 : Démarrer le serveur de développement :

  npm run start

7.2 : Ouvrir http://localhost:3000 dans votre navigateur

7.3 : Tester le flux d'authentification complet :
  - Cliquer sur « Login » pour s'authentifier avec Auth0
  - Consulter les informations de profil (le courriel sera affiché depuis Auth0)
  - Mettre à jour votre biographie et enregistrer
  - Actualiser la page — vous devriez rester connecté (grâce aux jetons d'actualisation)
  - Tester la fonctionnalité de déconnexion
  - Tester les appels RPC (Get Profile, Update Profile, Get Public Data)
  - Vérifier l'état de la connexion WebSocket
  - Tester la fonctionnalité de déconnexion

EXIGENCES DE SÉCURITÉ ET MEILLEURES PRATIQUES
- ✅ NE JAMAIS accepter des appels RPC non authentifiés pour les méthodes protégées
- ✅ TOUJOURS valider les signatures JWT à l'aide du JWKS d'Auth0
- ✅ Implémenter une gestion complète des erreurs pour les jetons expirés ou invalides
- ✅ Utiliser des variables d'environnement pour toute configuration sensible
- ✅ Valider toutes les entrées utilisateur avant le traitement
- ✅ Journaliser les événements de sécurité et les tentatives d'authentification
- ✅ Utiliser des connexions WebSocket sécurisées (WSS) en production
- ✅ Implémenter des politiques CORS appropriées
- ✅ Ajouter une limite de débit des requêtes pour une utilisation en production
- ✅ Assainir toutes les données avant leur stockage ou leur transmission

CONSEILS DE DÉPANNAGE
- Vérifier la console du navigateur pour les erreurs JavaScript
- Vérifier que le fichier .env contient la configuration Auth0 correcte
- S'assurer que les paramètres de l'application Auth0 correspondent à vos URL locales
- Confirmer que les scopes de l'API sont correctement configurés dans le Auth0 Dashboard
- Tester la connectivité WebSocket séparément si les appels RPC échouent
- Valider les jetons JWT à l'aide de jwt.io pour le débogage

Premiers pas

Ce guide de démarrage rapide montre comment ajouter l’authentification Auth0 à une application Cap’n Web. Vous créerez une application Web moderne reposant sur RPC avec une fonctionnalité de connexion sécurisée à l’aide du framework JavaScript de Cap’n Web et de l’Auth0 SPA SDK.
1

Créer un projet

Créez un nouveau projet Cap’n Web et mettez en place la structure de base :
mkdir capnweb-auth0-app && cd capnweb-auth0-app
Initialisez le projet et configurez-le pour utiliser les modules ES :
npm init -y && npm pkg set type="module"
Créez l’arborescence des dossiers du projet :
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
2

Installer les dépendances

Installez Cap’n Web et ses dépendances de base :
npm install capnweb ws dotenv
Installez les SDK Auth0 pour l’authentification et la vérification des jetons :
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js est utilisé côté client pour gérer l’authentification des utilisateurs, les flux de connexion et les jetons dans le navigateur.@auth0/auth0-api-js est utilisé côté serveur pour vérifier les jetons d’accès et valider les signatures JWT à l’aide du JWKS d’Auth0.
Configurez le script de démarrage dans package.json:
npm pkg set scripts.start="node server/index.js"
3

Configurez votre application Auth0

Ensuite, créez une nouvelle application sur votre locataire Auth0 et ajoutez les variables d’environnement à votre projet.Vous pouvez effectuer cette opération automatiquement en exécutant une commande CLI ou manuellement via le Auth0 Dashboard :
Exécutez la commande shell suivante à la racine de votre projet pour créer une application Auth0 et générer un fichier .env :
5

Créer le serveur

Créez le serveur Web Cap’n avec l’intégration 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();

// Configuration 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);
}

// Initialiser le client API Auth0 pour la vérification des jetons
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}`);
  }
}

// Service de profil - Cible RPC 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' };
  }
}

// Créer le serveur 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;
  }

  // Gérer le chemin racine et le callback 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 le SDK JS SPA Auth0 depuis 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 depuis 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');
});

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

wss.on('connection', (ws, req) => {
  // Traiter uniquement les connexions RPC sur le chemin /api
  if (req.url === '/api') {
    console.log('🔗 New Cap\'n Web RPC connection');
    
    // Créer une nouvelle instance de ProfileService pour cette connexion
    const profileService = new ProfileService();
    
    // Utiliser newWebSocketRpcSession de capnweb pour gérer la connexion
    newWebSocketRpcSession(ws, profileService);
  }
});

// Démarrer le serveur
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

Créer l’interface de l’application

Créez les fichiers HTML et JavaScript du frontend :
7

Lancez votre application

npm run start
Point de contrôleVous devriez maintenant disposer d’une page de connexion Auth0 entièrement fonctionnelle sur votre localhost

Utilisation avancée

Renforcez la sécurité en ajoutant une validation supplémentaire et une limite de débit à vos méthodes RPC :
server/profile-service.js
import rateLimit from 'express-rate-limit';

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // Limite de débit simple en mémoire
  }

  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
    };
  }
}
Mettez en place une gestion adéquate des connexions WebSocket avec reconnexion automatique :
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 {
      // Libérer la connexion existante, s’il y en a une
      if (this.api) {
        this.api[Symbol.dispose]();
      }

      // Créer une nouvelle session RPC 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;
  }
}

// Utilisation dans app.js
const connectionManager = new RpcConnectionManager(
  `${protocol}//${host}/api`,
  {
    maxReconnectAttempts: 5,
    reconnectDelay: 1000,
    onReconnect: async (api) => {
      // Actualiser l’interface utilisateur ou recharger les données après la reconnexion
      await displayProfile(api);
    }
  }
);

// Se connecter après l’authentification
if (isAuthenticated) {
  await connectionManager.connect();
  profileApi = connectionManager.getApi();
}
Remplacez le stockage en mémoire par une base de données en production :
server/database.js
import { Pool } from 'pg';

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

// Initialiser le schéma de la base de données
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: 'Profil mis à jour avec succès' };
  }
}

export { initializeDatabase, DatabaseProfileService };