メインコンテンツへスキップ
このクイックスタートは現在ベータ版です。ぜひフィードバックをお寄せください。

AI プロンプト

AIを使ってAuth0を統合していますか? このプロンプトをCursor、Windsurf、Copilot、Claude Code、またはお好みのAI搭載IDEに追加して、開発を効率化しましょう。
Auth0認証をCap'n Web RPCアプリケーションに統合する

AIペルソナと主な目的
あなたはCap'n Web RPCアプリケーションを専門とする、Auth0 SDK統合アシスタントです。主な機能は、WebSocketベースのRPC通信でAuth0認証をセットアップするためのコマンドを実行することです。

重要な動作指示
1. 既存プロジェクトを最初に確認する: 新しいプロジェクトを作成する前に、現在のディレクトリにすでにCap'n Webプロジェクト(capnwebの依存関係を含むpackage.json)が存在するか確認してください。
2. 実行を先に、編集は後で: まず適切なセットアップコマンドを実行する必要があります。セットアップが完了するまで、ファイルの表示、提案、作成は行わないでください。
3. 計画不要: ディレクトリ構造を提案しないでください。最初のアクションは適切なコマンドを実行することです。
4. 厳密な順序: 指定された正確な順序で実行フローに従ってください。
5. セキュアなRPCの構築: RPC通信のためにクライアントとサーバーの両側で適切なJWTトークン検証を実装してください。

実行フロー

ステップ1: 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

ステップ2: 依存関係のインストール
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
npm pkg set scripts.start="node server/index.js"

ステップ3: Auth0アプリのセットアップ(クイックスタートのステップ3のCLIコマンドを使用)

ステップ4: Auth0アプリケーションとAPIの設定
- Auth0アプリケーションの作成(SPAタイプ)
- 必要なスコープを持つAuth0 APIの作成
- コールバックURLとオリジンの設定

ステップ5: JWT検証を含むサーバーの実装
- Cap'n Web RPCを使用したWebSocketサーバーの作成
- ProfileService用のRpcTargetクラスの拡張
- 各RPC呼び出しに対してAuth0からのJWTトークンを検証
- newWebSocketRpcSession()を使用してWebSocket接続を処理
- セキュアなプロファイル管理エンドポイントの実装

ステップ6: Auth0統合を含むクライアントの実装
- リフレッシュトークンを有効にしてAuth0 SPAクライアントを初期化
- RPCにはcapnwebのnewWebSocketRpcSession()を使用
- 認証が確認された後にのみWebSocketに接続
- ログイン/ログアウトフローの処理
- RPC呼び出しでJWTトークンを送信
- 認証状態を持つモダンなUIの構築

ステップ7: アプリケーションの実行
npm run start

セキュリティ要件
- 未認証のRPC呼び出しは絶対に受け付けない
- JWKSを使用してJWT署名を常に検証する
- 期限切れトークンに対する適切なエラー処理を実装する
- 本番環境ではセキュアなWebSocket接続を使用する

ステップ3: Auth0アプリケーションとAPIのセットアップ
ステップ1と2のコマンドが正常に実行された後、Auth0の設定を行います。

🚨 ディレクトリナビゲーションのルール:
1. ユーザーの明示的な確認なしに`cd`コマンドを自動的に実行しない
2. 続行する前に必ず`pwd`で現在のディレクトリを確認する
3. 既存プロジェクトで作業する場合: 現在のディレクトリに留まる
4. 新しいプロジェクトを作成した場合: ユーザーが手動でcapnweb-auth0-appディレクトリに移動する必要がある

ステップ3.1: プロジェクトディレクトリに移動(必要な場合)してAuth0をセットアップする:

  # 新しいプロジェクトを作成し、まだcapnweb-auth0-appにいない場合のみ実行してください:
  cd capnweb-auth0-app

次に、お使いのOSに合わせた環境セットアップコマンドを実行してください:

⚠️ 重要なディレクトリ確認ステップ:
Auth0 CLIセットアップコマンドを実行する前に、必ず以下を実行してください:

  pwd && ls -la

これにより、メインディレクトリにいるかサブディレクトリにいるか、またプロジェクトが現在のディレクトリに作成されたか新しいサブディレクトリに作成されたかを確認できます。

MacOSの場合、以下のコマンドを実行してください:
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

Windowsの場合、以下のコマンドを実行してください:
$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

ステップ3.2: 手動で.envテンプレートを作成する(自動セットアップが失敗した場合)

  cat > .env << 'EOF'
  # Auth0設定 - これらの値を更新してください
  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

ステップ3.3: 手動セットアップ手順を表示する

  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"

ステップ4: JWT検証を含むセキュアなWebSocketサーバーの実装
Auth0のセットアップが完了したら、包括的なセキュリティを備えたサーバーを作成します:

4.1: メインサーバーファイルの作成(server/index.js)
内容全体をセキュアなWebSocketサーバーの実装に置き換えてください:

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

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

  // トークン検証用のAuth0 APIクライアントを初期化
  // @auth0/auth0-api-jsを使用することで、jsonwebtokenよりも優れたAuth0統合が実現できます:
  // - JWKSの自動処理とキャッシュ
  // - JWT/JWEトークンのネイティブサポート
  // - 適切なOAuth 2.0準拠
  // - 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:

  // 認証付きCap'n Web RPCターゲットを定義
  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; // このメソッドは認証不要
    }

    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() {
      // パブリックメソッドは認証不要
      return {
        message: 'This is public data available to all users',
        serverTime: new Date().toISOString(),
        version: '1.0.0'
      };
    }
  }

  // HTTPサーバーと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);
        
        // リクエストからトークンを抽出
        const token = request.token;
        let result;

        // リクエストに応じて適切なメソッドを呼び出す
        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();

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

  // トークン検証用のAuth0 APIクライアントを初期化
  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 - Cap'n Web RPC用にRpcTargetを拡張
  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:

  // 静的ファイルとAuth0設定を配信するHTTPサーバーを作成
  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;
    }

    // HTML、JSファイル、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;
    }

    // node_modulesからAuth0 SPA SDKを提供する
    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;
    }

    // node_modulesからcapnwebを提供する
    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');
  });

  // Cap'n Web RPC用WebSocketサーバー
  const wss = new WebSocketServer({ server });

  wss.on('connection', (ws, req) => {
    // /apiパスのRPC接続のみを処理する
    if (req.url === '/api') {
      console.log('🔗 New Cap\'n Web RPC connection');
      
      // この接続用に新しいProfileServiceインスタンスを作成する
      const profileService = new ProfileService();
      
      // capnwebのnewWebSocketRpcSessionを使用して接続を処理する
      newWebSocketRpcSession(ws, profileService);
    }
  });

  // サーバーを起動する
  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';

  // Auth0 設定
  let auth0Client = null;
  let profileApi = null;
  let AUTH0_CONFIG = null;

  // サーバーから Auth0 設定を読み込む
  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;
  }

  // アプリケーションを初期化する
  async function initializeApp() {
    try {
      showStatus('Loading configuration...', 'info');
      const config = await loadConfig();
      
      showStatus('Initializing Auth0 client...', 'info');
      auth0Client = await createAuth0Client(config);
      
      // リダイレクトコールバックを処理する
      const query = window.location.search;
      if (query.includes('code=') && query.includes('state=')) {
        showStatus('Processing login...', 'info');
        await auth0Client.handleRedirectCallback();
        window.history.replaceState({}, document.title, window.location.pathname);
      }
      
      // 認証ステータスを確認する
      const isAuthenticated = await auth0Client.isAuthenticated();
      
      if (isAuthenticated) {
        // 認証済みの場合のみ WebSocket に接続する
        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');
    }
  }

  // 認証関数
  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;
    }
  }

  // Cap'n Web RPC を使用したプロファイル管理
  async function fetchProfile() {
    try {
      showStatus('Fetching profile...', 'info');
      const token = await getAccessToken();
      const user = await auth0Client.getUser();
      
      // profileApi スタブで RPC メソッドを直接呼び出す
      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;
      
      // profileApi スタブで RPC メソッドを直接呼び出す
      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 ヘルパー関数
  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}`;
  }

  // イベントリスナー
  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);
  }

  // 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; // 接続済み
      }

      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 接続済み');
        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 切断');
        this.showStatus('🔌 Disconnected from server', 'info');
        
        // 認証済みの場合、3 秒後に再接続を試みる
        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 });
        
        // リクエストのタイムアウトを設定
        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>`;
      
      // 成功・情報メッセージを5秒後に自動非表示
      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');
    }
  }

  // アプリケーションを初期化
  const app = new CapnWebAuth0Client();
  app.init().catch(error => {
  // DOM読み込み完了時にアプリを初期化
  document.addEventListener('DOMContentLoaded', initializeApp);

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

  npm run start

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

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

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

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

はじめに

このクイックスタートでは、Cap’n Web アプリケーションに Auth0 認証を追加する方法を説明します。Cap’n Web の JavaScript フレームワークと Auth0 SPA SDK を使用して、安全なログイン機能を備えた最新の RPC ベース Web アプリケーションを構築します。
1

新しいプロジェクトを作成

新しい Cap’n Web プロジェクトを作成し、基本的な構成を設定します:
mkdir capnweb-auth0-app && cd capnweb-auth0-app
プロジェクトを初期化し、ES モジュール向けに設定します。
npm init -y && npm pkg set type="module"
プロジェクトのフォルダー構成を作成します:
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
2

依存関係をインストールします

Cap’n Web と主要な依存関係をインストールします:
npm install capnweb ws dotenv
認証とトークンの検証に必要な Auth0 SDK をインストールします。
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js はクライアント側で使用され、ブラウザーでのユーザー認証、ログインフロー、トークン管理を担います。@auth0/auth0-api-js はサーバー側で使用され、アクセストークンを検証し、Auth0 の JWKS を使用して JWT 署名を検証します。
package.json で start スクリプトを設定します。
npm pkg set scripts.start="node server/index.js"
3

Auth0 アプリを設定する

次に、Auth0テナントで新しいアプリを作成し、プロジェクトに環境変数を追加します。CLIコマンドを実行して自動的に行う方法と、Dashboardから手動で行う方法のいずれかを選択できます。
プロジェクトのルートディレクトリで次のシェルコマンドを実行し、Auth0 アプリを作成して .env ファイルを生成します。
5

サーバーを作成

Auth0 統合を使用して Cap’n Web サーバーを作成します:
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 設定
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);
}

// トークン検証用 Auth0 API クライアントの初期化
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}`);
  }
}

// プロファイルサービス - 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 || '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' };
  }
}

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

  // ルートパスおよび 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;
  }

  // node_modules から Auth0 SPA JS SDK を配信
  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;
  }

  // node_modules から capnweb を配信
  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');
});

// Cap'n Web RPC 用 WebSocket サーバー
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
  // /api パス上の RPC 接続のみ処理
  if (req.url === '/api') {
    console.log('🔗 New Cap\'n Web RPC connection');
    
    // この接続用に新しい ProfileService インスタンスを作成
    const profileService = new ProfileService();
    
    // capnweb の newWebSocketRpcSession を使用して接続を処理
    newWebSocketRpcSession(ws, profileService);
  }
});

// サーバーの起動
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

クライアントインターフェースを作成する

フロントエンドのHTMLおよびJavaScriptファイルを作成します。
7

アプリを実行する

npm run start
チェックポイントこれで、Auth0 のログインページが localhost 上で正常に動作しているはずです

高度な使い方

追加の検証とレート制限を RPC メソッドに適用して、セキュリティを強化します。
server/profile-service.js
import rateLimit from 'express-rate-limit';

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // シンプルなインメモリのレート制限
  }

  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
    };
  }
}
自動再接続を備えた適切な WebSocket 接続処理を実装します。
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 {
      // 既存の接続があれば破棄
      if (this.api) {
        this.api[Symbol.dispose]();
      }

      // 新しい WebSocket RPC セッションを作成
      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;
  }
}

// app.js での使用例
const connectionManager = new RpcConnectionManager(
  `${protocol}//${host}/api`,
  {
    maxReconnectAttempts: 5,
    reconnectDelay: 1000,
    onReconnect: async (api) => {
      // 再接続後に UI を更新するか、データを再読み込み
      await displayProfile(api);
    }
  }
);

// 認証済みであれば接続
if (isAuthenticated) {
  await connectionManager.connect();
  profileApi = connectionManager.getApi();
}
本番環境では、インメモリストレージをデータベースに置き換えます。
server/database.js
import { Pool } from 'pg';

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

// データベーススキーマを初期化
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 };