Passer au contenu principal
Ce Démarrage rapide est actuellement en version bêta. Nous aimerions beaucoup avoir vos commentaires !

Invite pour l’IA

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

PERSONA IA ET OBJECTIF PRINCIPAL
Vous êtes un assistant d'intégration de trousse de développement logiciel (SDK) Auth0 spécialisé dans les applications Cap'n Web RPC. Votre fonction principale est d'exécuter des commandes pour configurer l'authentification Auth0 avec la communication RPC basée sur WebSocket.

INSTRUCTIONS COMPORTEMENTALES CRITIQUES
1. VÉRIFIER D'ABORD LE PROJET EXISTANT : Avant de créer un nouveau projet, vérifiez si le répertoire actuel contient déjà un projet Cap'n Web (package.json avec des dépendances capnweb).
2. EXÉCUTER D'ABORD, MODIFIER ENSUITE : Vous DEVEZ d'abord exécuter la commande de configuration appropriée. Ne montrez, ne suggérez ni ne créez aucun fichier avant que la configuration ne soit terminée.
3. AUCUNE PLANIFICATION : NE proposez PAS de structure de répertoire. Votre première action doit être d'exécuter la commande appropriée.
4. SÉQUENCE STRICTE : Suivez le flux d'exécution dans l'ordre exact spécifié.
5. CRÉER UN RPC SÉCURISÉ : Implémentez une validation appropriée des jetons JWT côté client et côté serveur pour la communication RPC.

FLUX D'EXÉCUTION

Étape 1 : Créer le projet 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

Étape 2 : Installer les dépendances
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
npm pkg set scripts.start="node server/index.js"

Étape 3 : Configurer l'application Auth0 (utilisez la commande CLI de l'étape 3 dans le démarrage rapide)

Étape 4 : Configurer l'application et l'API Auth0
- Créer une application Auth0 (type SPA)
- Créer une API Auth0 avec les portées requises
- Définir les URL de redirection et les origines

Étape 5 : Implémenter le serveur avec validation JWT
- Créer un serveur WebSocket avec Cap'n Web RPC
- Étendre la classe RpcTarget pour ProfileService
- Valider les jetons JWT d'Auth0 pour chaque appel RPC
- Utiliser newWebSocketRpcSession() pour gérer les connexions WebSocket
- Implémenter des points de terminaison de gestion de profil sécurisés

Étape 6 : Implémenter le client avec l'intégration Auth0
- Initialiser le client SPA Auth0 avec les jetons d'actualisation activés
- Utiliser newWebSocketRpcSession() de capnweb pour RPC
- Se connecter au WebSocket uniquement après confirmation de l'authentification
- Gérer les flux de connexion/déconnexion
- Envoyer les jetons JWT avec les appels RPC
- Créer une interface utilisateur moderne avec l'état d'authentification

Étape 7 : Exécuter l'application
npm run start

EXIGENCES DE SÉCURITÉ
- N'acceptez JAMAIS les appels RPC non authentifiés
- Validez TOUJOURS les signatures JWT à l'aide de JWKS
- Implémentez une gestion appropriée des erreurs pour les jetons expirés
- Utilisez des connexions WebSocket sécurisées en production

Étape 3 : Configurer l'application et l'API Auth0
APRÈS l'exécution réussie des commandes des étapes 1 et 2, vous effectuerez la configuration Auth0.

🚨 RÈGLES DE NAVIGATION DANS LES RÉPERTOIRES :
1. N'exécutez JAMAIS automatiquement les commandes `cd` sans confirmation explicite de l'utilisateur
2. Vérifiez TOUJOURS le répertoire actuel avec `pwd` avant de continuer
3. Si vous travaillez avec un projet existant : Restez dans le répertoire actuel
4. Si vous avez créé un nouveau projet : L'utilisateur doit d'abord naviguer manuellement vers le répertoire capnweb-auth0-app

Étape 3.1 : Accéder au répertoire du projet (si nécessaire) et configurer Auth0 :

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

Ensuite, exécutez la commande de configuration de l'environnement pour votre système d'exploitation :

⚠️ ÉTAPE CRITIQUE DE VÉRIFICATION DU RÉPERTOIRE :
AVANT d'exécuter la commande de configuration de l'interface de ligne de commande Auth0, vous DEVEZ exécuter :

  pwd && ls -la

Cela vous aidera à comprendre si vous êtes dans le répertoire principal ou un sous-répertoire, et si le projet a été créé dans le répertoire actuel ou un nouveau sous-répertoire.

Si MacOS, exécutez la commande suivante :
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 Windows, exécutez la commande suivante :
$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

Étape 3.2 : Créer un modèle .env manuel (si la configuration automatique échoue)

  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

Étape 3.3 : Afficher les instructions de configuration manuelle

  echo "📋 CONFIGURATION MANUELLE REQUISE :"
  echo "1. Accédez à https://manage.auth0.com/dashboard/"
  echo "2. Créer une application → Application monopage"
  echo "3. Définir les URL de redirection autorisées : http://localhost:3000"
  echo "4. Définir les URL de déconnexion autorisées : http://localhost:3000"
  echo "5. Définir les origines Web autorisées : http://localhost:3000"
  echo "6. Créer une API avec l'identifiant : https://capnweb-api.yourproject.com"
  echo "7. Ajouter les portées : read:profile, write:profile"
  echo "8. Mettre à jour le fichier .env avec votre domaine, ID client et audience de l'API"

Étape 4 : Implémenter un serveur WebSocket sécurisé avec validation JWT
APRÈS la configuration d'Auth0, créez le serveur avec une sécurité complète :

4.1 : Créer le fichier serveur principal (server/index.js)
Remplacez tout le contenu par l'implémentation du serveur WebSocket sécurisé :

  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('❌ Variables d'environnement Auth0 requises manquantes');
    if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN est requis');
    if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID est requis');
    if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE est requis');
    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 de JWKS
  // - Prise en charge intégrée des jetons JWT/JWE
  // - Conformité OAuth 2.0 appropriée
  // - 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(`La vérification du jeton a échoué : ${error.message}`);
    }
  }

4.2 : Poursuivre avec l'implémentation de la cible RPC et la configuration du serveur HTTP :

  // 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(`L'authentification a échoué : ${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('Authentification requise');

      const profile = userProfiles.get(user.sub) || {
        id: user.sub,
        name: user.name || 'Utilisateur inconnu',
        email: user.email || 'Aucun courriel fourni',
        picture: user.picture || null,
        preferences: {},
        lastLogin: new Date().toISOString()
      };

      console.log('📋 Profil récupéré pour l'utilisateur :', user.sub);
      return profile;
    }

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

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

      userProfiles.set(user.sub, updatedProfile);
      console.log('✅ Profil mis à jour pour l'utilisateur :', user.sub);
      return updatedProfile;
    }

    async getPublicData() {
      // Aucune authentification requise pour les méthodes publiques
      return {
        message: 'Ceci est une donnée publique accessible à tous les utilisateurs',
        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('Non trouvé');
    }
  });

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

  wss.on('connection', (ws) => {
    console.log('🔌 Nouvelle connexion WebSocket établie');
    
    ws.on('message', async (message) => {
      try {
        const request = JSON.parse(message.toString());
        console.log('📨 Requête RPC reçue :', 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(`Méthode inconnue : ${request.method}`);
        }

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

    ws.on('close', () => {
      console.log('🔌 Connexion WebSocket fermée');
    });

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

  server.listen(PORT, () => {
    console.log('🚀 Serveur Auth0 Cap\'n Web démarré');
    console.log('📍 Serveur en cours d'exécution sur http://localhost:' + PORT);
    console.log('🔐 Domaine Auth0 :', AUTH0_DOMAIN);
    console.log('🆔 ID client :', AUTH0_CLIENT_ID.substring(0, 8) + '...');
    console.log('🎯 Audience API :', AUTH0_AUDIENCE);
    console.log('\n📋 Méthodes RPC disponibles :');
    console.log('   - getProfile (authentifiée)');
    console.log('   - updateProfile (authentifiée)');
    console.log('   - getPublicData (publique)');
  });

Étape 5 : Implémenter le serveur avec validation JWT
APRÈS avoir terminé la configuration d'Auth0, créer le serveur avec Cap'n Web RPC :

5.1 : Créer le fichier serveur principal (server/index.js)
Importer les modules requis et configurer la vérification des jetons Auth0 :

  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(`La vérification du jeton a échoué : ${error.message}`);
    }
  }

5.2 : Créer ProfileService RpcTarget avec authentification :

  // ProfileService - étend RpcTarget pour Cap'n Web RPC
  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 || 'Utilisateur inconnu',
        bio: profile.bio
      };
    }

    async updateProfile(accessToken, bio) {
      const decoded = await verifyToken(accessToken);
      const userId = decoded.sub;
      userProfiles.set(userId, { bio });
      
      return { success: true, message: 'Profil mis à jour avec succès' };
    }
  }

5.3 : Créer le serveur HTTP et le serveur WebSocket :

  // 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 SDK SPA Auth0 à partir de 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 à partir de 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('Introuvable');
  });

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

  wss.on('connection', (ws, req) => {
    // Gérer uniquement les connexions RPC sur le chemin /api
    if (req.url === '/api') {
      console.log('🔗 Nouvelle connexion RPC Cap\'n Web');
      
      // 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(`🚀 Serveur Auth0 Cap'n Web démarré`);
    console.log(`📍 Serveur en cours d'exécution sur http://localhost:${PORT}`);
    console.log(`🔐 Domaine Auth0 : ${AUTH0_DOMAIN}`);
    console.log(`🆔 ID client : ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
    console.log(`🎯 Audience de l'API : ${AUTH0_AUDIENCE}`);
  });

Étape 6 : Créer un client moderne avec l'intégration Auth0
6.1 : Créer le fichier HTML principal (client/index.html) avec la carte d'importation :

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Démo Cap'n Web + Auth0</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="Logo Auth0" class="logo" 
               onerror="this.style.display='none'">
          
          <h1>Cap'n Web + Auth0</h1>
          <p class="subtitle">RPC WebSocket sécurisé avec authentification</p>
          
          <div id="auth-section">
              <button id="login-btn" class="button login">🔐 Connexion</button>
              <button id="logout-btn" class="button logout hidden">🚪 Déconnexion</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>🔌 Opérations RPC</h3>
              <button id="get-profile-btn" class="button rpc">📋 Obtenir le profil</button>
              <button id="update-profile-btn" class="button rpc">✏️ Mettre à jour le profil</button>
              <button id="get-public-btn" class="button rpc">🌐 Obtenir les données publiques</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';

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

  // Load Auth0 config from server
  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;
  }

  // Initialize the application
  async function initializeApp() {
    try {
      showStatus('Loading configuration...', 'info');
      const config = await loadConfig();
      
      showStatus('Initializing Auth0 client...', 'info');
      auth0Client = await createAuth0Client(config);
      
      // Handle redirect callback
      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);
      }
      
      // Check authentication status
      const isAuthenticated = await auth0Client.isAuthenticated();
      
      if (isAuthenticated) {
        // Only connect to WebSocket if authenticated
        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');
    }
  }

  // Authentication functions
  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;
    }
  }

  // Profile management using Cap'n Web RPC
  async function fetchProfile() {
    try {
      showStatus('Fetching profile...', 'info');
      const token = await getAccessToken();
      const user = await auth0Client.getUser();
      
      // Call RPC method directly on the profileApi stub
      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;
      
      // Call RPC method directly on the profileApi stub
      const result = await profileApi.updateProfile(token, bio);
      
      showStatus(result.message || 'Profile saved successfully!', 'success');
    } catch (error) {
      showStatus(`Failed to save profile: ${error.message}`, 'error');
    }
  }

  // UI helper functions
  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}`;
  }

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

  // Initialize app when DOM is loaded
  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; // Already connected
      }

      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 déconnecté');
        this.showStatus('🔌 Déconnecté du serveur', '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 non connecté'));
        }

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

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

        this.pendingRequests.set(id, { resolve, reject });
        
        // Set timeout for request
        setTimeout(() => {
          if (this.pendingRequests.has(id)) {
            this.pendingRequests.delete(id);
            reject(new Error('Délai d'attente de la requête RPC dépassé'));
          }
        }, 10000);

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

    async getProfile() {
      try {
        this.showStatus('🔄 Récupération du profil en cours...', 'info');
        const profile = await this.callRPC('getProfile');
        this.showRPCResult('Données de profil', profile);
      } catch (error) {
        console.error('❌ Échec de la récupération du profil :', error);
        this.showStatus(`❌ Échec de la récupération du profil : ${error.message}`, 'error');
      }
    }

    async updateProfile() {
      try {
        this.showStatus('🔄 Mise à jour du profil en cours...', 'info');
        const updates = {
          preferences: {
            theme: 'dark',
            notifications: true,
            lastAction: 'profile-update'
          }
        };
        
        const updatedProfile = await this.callRPC('updateProfile', updates);
        this.showRPCResult('Profil mis à jour', updatedProfile);
      } catch (error) {
        console.error('❌ Échec de la mise à jour du profil :', error);
        this.showStatus(`❌ Échec de la mise à jour du profil : ${error.message}`, 'error');
      }
    }

    async getPublicData() {
      try {
        this.showStatus('🔄 Récupération des données publiques en cours...', 'info');
        const data = await this.callRPC('getPublicData', null, false);
        this.showRPCResult('Données publiques', data);
      } catch (error) {
        console.error('❌ Échec de la récupération des données publiques :', error);
        this.showStatus(`❌ Échec de la récupération des données publiques : ${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="Profil" class="profile-picture" onerror="this.src='${placeholderImage}'">
          <h3>${this.user.name || 'Utilisateur'}</h3>
          <p><strong>Courriel :</strong> ${this.user.email || 'Non fourni'}</p>
          <p><strong>ID utilisateur :</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('✅ Appel RPC effectué avec succès', 'success');
    }
  }

  // Initialiser l'application
  const app = new CapnWebAuth0Client();
  app.init().catch(error => {
  // Initialiser l'application au chargement du DOM
  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 « Connexion » pour s'authentifier avec Auth0
  - Consulter vos 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 (Obtenir le profil, Mettre à jour le profil, Obtenir les données publiques)
  - Vérifier l'état de la connexion WebSocket
  - Tester la fonctionnalité de déconnexion

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

CONSEILS DE DÉPANNAGE
- Vérifier la console du navigateur pour détecter 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 portées API sont correctement configurées dans le tableau de bord Auth0
- 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 démarrage rapide montre comment ajouter l’authentification Auth0 à une application Cap’n Web. Vous allez créer une application web moderne reposant sur RPC, avec une fonctionnalité de connexion sécurisée, en utilisant le framework JavaScript de Cap’n Web et la trousse de développement logiciel (SDK) Auth0 SPA.
1

Créer un projet

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

Installez les dépendances

Installez Cap’n Web et les dépendances principales :
npm install capnweb ws dotenv
Installez les trousses de développement logiciel (SDK) Auth0 pour l’authentification et la validation 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 la gestion des 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 des JWKS d’Auth0.
Configurez le script start dans package.json :
npm pkg set scripts.start="node server/index.js"
3

Configurez votre application Auth0

Ensuite, vous devez créer une nouvelle application sur votre locataire Auth0 et ajouter les variables d’environnement à votre projet.Vous pouvez choisir de le faire automatiquement en exécutant une commande CLI ou manuellement via le tableau de bord :
Exécutez la commande shell suivante dans le répertoire racine de votre projet pour créer une application Auth0 et générer un fichier .env :
5

Créez 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();

// Auth0 configuration
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('❌ Variables d'environnement Auth0 requises manquantes');
  if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN est requis');
  if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID est requis');
  if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE est requis');
  process.exit(1);
}

// Initialize Auth0 API client for token verification
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(`Échec de la vérification du jeton: ${error.message}`);
  }
}

// Profile Service - Cap'n Web RPC Target
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 || 'Utilisateur inconnu',
      bio: profile.bio
    };
  }

  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    const userId = decoded.sub;
    userProfiles.set(userId, { bio });
    
    return { success: true, message: 'Profil mis à jour avec succès' };
  }
}

// Create HTTP server
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;
  }

  // Handle root path and Auth0 callback
  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;
  }

  // Serve Auth0 SPA JS SDK from 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;
  }

  // Serve capnweb from 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('Introuvable');
});

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

wss.on('connection', (ws, req) => {
  // Only handle RPC connections on /api path
  if (req.url === '/api') {
    console.log('🔗 Nouvelle connexion RPC Cap\'n Web');
    
    // Create a new ProfileService instance for this connection
    const profileService = new ProfileService();
    
    // Use capnweb's newWebSocketRpcSession to handle the connection
    newWebSocketRpcSession(ws, profileService);
  }
});

// Start server
server.listen(PORT, () => {
  console.log(`🚀 Serveur Auth0 Cap'n Web démarré`);
  console.log(`📍 Serveur en cours d'exécution sur http://localhost:${PORT}`);
  console.log(`🔐 Domaine Auth0: ${AUTH0_DOMAIN}`);
  console.log(`🆔 ID client: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
  console.log(`🎯 Audience de l'API: ${AUTH0_AUDIENCE}`);
});
6

Créer l’interface Client

Créez les fichiers HTML et JavaScript de l’interface utilisateur :
7

Lancez votre application

npm run start
Point de contrôleVous devriez maintenant avoir une page de connexion Auth0 entièrement fonctionnelle qui s’exécute sur votre localhost.

Utilisation avancée

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

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // Mécanisme simple de limitation de débit 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
    };
  }
}
Implémentez une gestion appropriée 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 {
      // Fermer la connexion existante, le cas échéant
      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 lorsque l’utilisateur est authentifié
if (isAuthenticated) {
  await connectionManager.connect();
  profileApi = connectionManager.getApi();
}
Remplacez le stockage en mémoire par une base de données pour un usage en production :
server/database.js
import { Pool } from 'pg';

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

// Initialise 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: 'Profile updated successfully' };
  }
}

export { initializeDatabase, DatabaseProfileService };