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

# Cap'n Web アプリケーションにログイン機能を追加する

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

<Callout icon="pencil" color="#FFC107" iconType="solid">
  このクイックスタートは現在**ベータ版**です。ぜひご意見・ご感想をお聞かせください。
</Callout>

<Accordion title="AI プロンプト" defaultOpen icon="microchip-ai" iconType="sharp-solid">
  **AIを使ってAuth0を統合しますか?** このプロンプトをCursor、Windsurf、Copilot、Claude Code、またはお好みのAI搭載IDEに追加して、開発を高速化しましょう。

  ```markdown expandable theme={null}
  Cap'n Web RPCアプリケーションにAuth0認証を統合する

  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 "📋 手動セットアップが必要:"
    echo "1. https://manage.auth0.com/dashboard/ にアクセス"
    echo "2. アプリケーションを作成 → シングルページアプリケーション"
    echo "3. 許可されたコールバックURLを設定: http://localhost:3000"
    echo "4. 許可されたログアウトURLを設定: http://localhost:3000"
    echo "5. 許可されたWebオリジンを設定: http://localhost:3000"
    echo "6. 識別子を使用してAPIを作成: https://capnweb-api.yourproject.com"
    echo "7. スコープを追加: read:profile, write:profile"
    echo "8. ドメイン、クライアントID、APIオーディエンスで.envファイルを更新"

  ステップ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('❌ 必要なAuth0環境変数が不足しています');
      if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAINが必要です');
      if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_IDが必要です');
      if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCEが必要です');
      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(`トークンの検証に失敗しました: ${error.message}`);
      }
    }

  4.2: RPCターゲットの実装とHTTPサーバーのセットアップを続けます:

    // 認証機能を持つ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(`認証に失敗しました: ${error.message}`);
          }
        }
        return null; // このメソッドには認証が不要
      }

      async getProfile(token) {
        const user = await this.authenticate('getProfile', token);
        if (!user) throw new Error('認証が必要です');

        const profile = userProfiles.get(user.sub) || {
          id: user.sub,
          name: user.name || '不明なユーザー',
          email: user.email || 'メールアドレスが提供されていません',
          picture: user.picture || null,
          preferences: {},
          lastLogin: new Date().toISOString()
        };

        console.log('📋 ユーザーのプロファイルを取得しました:', user.sub);
        return profile;
      }

      async updateProfile(token, updates) {
        const user = await this.authenticate('updateProfile', token);
        if (!user) throw new Error('認証が必要です');

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

        userProfiles.set(user.sub, updatedProfile);
        console.log('✅ ユーザーのプロファイルを更新しました:', user.sub);
        return updatedProfile;
      }

      async getPublicData() {
        // 公開メソッドには認証が不要
        return {
          message: 'これはすべてのユーザーが利用できる公開データです',
          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('見つかりません');
      }
    });

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

    wss.on('connection', (ws) => {
      console.log('🔌 新しいWebSocket接続が確立されました');
      
      ws.on('message', async (message) => {
        try {
          const request = JSON.parse(message.toString());
          console.log('📨 RPCリクエストを受信しました:', 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(`不明なメソッド: ${request.method}`);
          }

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

      ws.on('close', () => {
        console.log('🔌 WebSocket接続が閉じられました');
      });

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

    server.listen(PORT, () => {
      console.log('🚀 Cap\'n Web Auth0サーバーを起動しました');
      console.log('📍 サーバーはhttp://localhost:' + PORTで実行中です');
      console.log('🔐 Auth0ドメイン:', AUTH0_DOMAIN);
      console.log('🆔 クライアントID:', AUTH0_CLIENT_ID.substring(0, 8) + '...');
      console.log('🎯 APIオーディエンス:', AUTH0_AUDIENCE);
      console.log('\n📋 利用可能なRPCメソッド:');
      console.log('   - getProfile (認証済み)');
      console.log('   - updateProfile (認証済み)');
      console.log('   - getPublicData (公開)');
    });

  ステップ5: JWT検証を使用したサーバーの実装
  Auth0のセットアップが完了した後、Cap'n Web RPCを使用してサーバーを作成します:

  5.1: メインサーバーファイル(server/index.js)を作成
  必要なモジュールをインポートし、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();

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

    // 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(`トークンの検証に失敗しました: ${error.message}`);
      }
    }

  5.2: 認証機能を持つProfileService RpcTargetを作成:

    // 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 || '不明なユーザー',
          bio: profile.bio
        };
      }

      async updateProfile(accessToken, bio) {
        const decoded = await verifyToken(accessToken);
        const userId = decoded.sub;
        userProfiles.set(userId, { bio });
        
        return { success: true, message: 'プロファイルが正常に更新されました' };
      }
    }

  5.3: HTTPサーバーとWebSocketサーバーを作成:

    // 静的ファイルと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('見つかりません');
    });

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

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

    // サーバーを起動
    server.listen(PORT, () => {
      console.log(`🚀 Cap'n Web Auth0サーバーを起動しました`);
      console.log(`📍 サーバーがhttp://localhost:${PORT}で実行中です`);
      console.log(`🔐 Auth0ドメイン: ${AUTH0_DOMAIN}`);
      console.log(`🆔 クライアントID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
      console.log(`🎯 APIオーディエンス: ${AUTH0_AUDIENCE}`);
    });

  ステップ6: Auth0統合を使用したモダンクライアントを作成
  6.1: インポートマップを含むメインHTMLファイル(client/index.html)を作成:

    <!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デモ</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">認証を使用したセキュアなWebSocket RPC</p>
            
            <div id="auth-section">
                <button id="login-btn" class="button login">🔐 ログイン</button>
                <button id="logout-btn" class="button logout hidden">🚪 ログアウト</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操作</h3>
                <button id="get-profile-btn" class="button rpc">📋 プロファイルを取得</button>
                <button id="update-profile-btn" class="button rpc">✏️ プロファイルを更新</button>
                <button id="get-public-btn" class="button rpc">🌐 公開データを取得</button>
            </div>

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

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

  6.2: JavaScriptクライアントアプリケーションを作成する (client/app.js)
  capnwebのnewWebSocketRpcSessionとAuth0 SDKを適切なESモジュールインポートで使用します:

    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('設定を読み込んでいます...', 'info');
        const config = await loadConfig();
        
        showStatus('Auth0クライアントを初期化しています...', 'info');
        auth0Client = await createAuth0Client(config);
        
        // Handle redirect callback
        const query = window.location.search;
        if (query.includes('code=') && query.includes('state=')) {
          showStatus('ログインを処理しています...', '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('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('初期化エラー:', error);
        showStatus(`初期化に失敗しました: ${error.message}`, 'error');
      }
    }

    // Authentication functions
    async function login() {
      try {
        showStatus('Auth0にリダイレクトしています...', 'info');
        await auth0Client.loginWithRedirect();
      } catch (error) {
        showStatus(`ログインに失敗しました: ${error.message}`, 'error');
      }
    }

    async function logout() {
      try {
        if (profileApi) {
          profileApi[Symbol.dispose]();
        }
        await auth0Client.logout({
          logoutParams: { returnTo: window.location.origin }
        });
      } catch (error) {
        showStatus(`ログアウトに失敗しました: ${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('プロファイルを取得しています...', '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 || 'メールアドレスがありません';
        document.getElementById('bioTextarea').value = profile.bio || '';
        
        showStatus('プロファイルを正常に読み込みました!', 'success');
      } catch (error) {
        showStatus(`プロファイルの取得に失敗しました: ${error.message}`, 'error');
      }
    }

    async function saveProfile() {
      try {
        showStatus('プロファイルを保存しています...', '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 || 'プロファイルを正常に保存しました!', 'success');
      } catch (error) {
        showStatus(`プロファイルの保存に失敗しました: ${error.message}`, 'error');
      }
    }

    // UI helper functions
    function showAuthSection() {
      document.getElementById('authSection').style.display = 'block';
      document.getElementById('profileSection').style.display = 'none';
      showStatus('ログイン準備完了', '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('✅ アプリケーションを正常に初期化しました', 'success');
        } catch (error) {
          console.error('❌ 初期化に失敗しました:', error);
          this.showStatus(`❌ 初期化に失敗しました: ${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('🔄 Auth0にリダイレクトしています...', 'info');
          await this.auth0Client.loginWithRedirect();
        } catch (error) {
          console.error('❌ ログインに失敗しました:', error);
          this.showStatus(`❌ ログインに失敗しました: ${error.message}`, 'error');
        }
      }

      async logout() {
        try {
          this.closeWebSocket();
          await this.auth0Client.logout({
            logoutParams: {
              returnTo: window.location.origin
            }
          });
        } catch (error) {
          console.error('❌ ログアウトに失敗しました:', error);
          this.showStatus(`❌ ログアウトに失敗しました: ${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('🔌 Cap\'n Webサーバーに接続しました', '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('❌ WebSocketメッセージの解析に失敗しました:', error);
          }
        };

        this.ws.onerror = (error) => {
          console.error('❌ WebSocketエラー:', error);
          this.showStatus('❌ WebSocket接続エラー', 'error');
        };

        this.ws.onclose = () => {
          console.log('🔌 WebSocketから切断しました');
          this.showStatus('🔌 サーバーから切断しました', '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が接続されていません'));
          }

          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('RPCリクエストがタイムアウトしました'));
            }
          }, 10000);

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

      async getProfile() {
        try {
          this.showStatus('🔄 プロファイルを取得中...', 'info');
          const profile = await this.callRPC('getProfile');
          this.showRPCResult('プロファイルデータ', profile);
        } catch (error) {
          console.error('❌ プロファイルの取得に失敗しました:', error);
          this.showStatus(`❌ プロファイルの取得に失敗しました: ${error.message}`, 'error');
        }
      }

      async updateProfile() {
        try {
          this.showStatus('🔄 プロファイルを更新中...', 'info');
          const updates = {
            preferences: {
              theme: 'dark',
              notifications: true,
              lastAction: 'profile-update'
            }
          };
          
          const updatedProfile = await this.callRPC('updateProfile', updates);
          this.showRPCResult('更新されたプロファイル', updatedProfile);
        } catch (error) {
          console.error('❌ プロファイルの更新に失敗しました:', error);
          this.showStatus(`❌ プロファイルの更新に失敗しました: ${error.message}`, 'error');
        }
      }

      async getPublicData() {
        try {
          this.showStatus('🔄 公開データを取得中...', 'info');
          const data = await this.callRPC('getPublicData', null, false);
          this.showRPCResult('公開データ', data);
        } catch (error) {
          console.error('❌ 公開データの取得に失敗しました:', error);
          this.showStatus(`❌ 公開データの取得に失敗しました: ${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="プロファイル" class="profile-picture" onerror="this.src='${placeholderImage}'">
            <h3>${this.user.name || 'ユーザー'}</h3>
            <p><strong>メール:</strong> ${this.user.email || '未提供'}</p>
            <p><strong>ユーザー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>`;
        
        // Auto-hide success and info messages after 5 seconds
        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呼び出しが正常に完了しました', 'success');
      }
    }

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

  ステップ7: アプリケーションのテストと実行
  7.1: 開発サーバーを起動します:

    npm run start

  7.2: ブラウザで http://localhost:3000 を開きます

  7.3: 完全な認証フローをテストします:
    - 「ログイン」をクリックしてAuth0で認証します
    - プロファイル情報を表示します(メールはAuth0から表示されます)
    - 自己紹介を更新して保存します
    - ページを更新します - リフレッシュトークンによりログイン状態が維持されます
    - ログアウト機能をテストします
    - RPC呼び出しをテストします(プロファイルの取得、プロファイルの更新、公開データの取得)
    - WebSocket接続ステータスを確認します
    - ログアウト機能をテストします

  セキュリティ要件とベストプラクティス
  - ✅ 保護されたメソッドに対して認証されていないRPC呼び出しを決して受け入れない
  - ✅ Auth0のJWKSを使用してJWT署名を常に検証する
  - ✅ 期限切れまたは無効なトークンに対する包括的なエラー処理を実装する
  - ✅ すべての機密設定に環境変数を使用する
  - ✅ 処理前にすべてのユーザー入力を検証する
  - ✅ セキュリティイベントと認証試行をログに記録する
  - ✅ 本番環境では安全なWebSocket接続(WSS)を使用する
  - ✅ 適切なCORSポリシーを実装する
  - ✅ 本番環境用にリクエストレート制限を追加する
  - ✅ 保存または送信前にすべてのデータをサニタイズする

  トラブルシューティングのヒント
  - ブラウザコンソールでJavaScriptエラーを確認する
  - .envファイルに正しいAuth0設定が含まれていることを確認する
  - Auth0アプリケーション設定がローカルURLと一致していることを確認する
  - Auth0 DashboardでAPIスコープが適切に設定されていることを確認する
  - RPC呼び出しが失敗する場合は、WebSocket接続を個別にテストする
  - デバッグのためにjwt.ioを使用してJWTトークンを検証する
  ```
</Accordion>

<div id="get-started">
  ## はじめに
</div>

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

<Steps>
  <Step title="新しいプロジェクトを作成" stepNumber={1}>
    新しい Cap'n Web プロジェクトを新規作成し、基本的な構造をセットアップします。

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

    プロジェクトを初期化し、ES モジュール用に設定します。

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

    プロジェクトのフォルダ構成を作成します：

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

  <Step title="依存関係のインストール" stepNumber={2}>
    Cap'n Web とコアの依存関係をインストールします：

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

    認証およびトークン検証用の Auth0 ソフトウェア開発キット (SDK) をインストールします:

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

    <Info>
      **@auth0/auth0-spa-js** は、クライアント側でユーザー認証、ログインフロー、ブラウザー内でのトークン管理を行うために使用します。

      **@auth0/auth0-api-js** は、サーバー側でアクセストークンを検証し、Auth0 の JWKS を使用して JWT 署名を検証するために使用します。
    </Info>

    package.json で start スクリプトを設定します。

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

  <Step title="Auth0 アプリケーションを設定する" stepNumber={3}>
    次に、Auth0テナントで新しいアプリケーションを作成し、環境変数をプロジェクトに追加する必要があります。

    CLIコマンドを実行して自動的に実行するか、ダッシュボードから手動で実行するかを選択できます:

    <Tabs>
      <Tab title="CLI">
        プロジェクトのルートディレクトリで次のシェルコマンドを実行して、Auth0 アプリケーションを作成し、`.env` ファイルを作成します。

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

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

      <Tab title="ダッシュボード">
        開始する前に、プロジェクトのルートディレクトリに `.env` ファイルを作成してください。

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

        1. [Auth0 Dashboard](https://manage.auth0.com/dashboard/) に移動します
        2. **Applications** > **Applications** > **Create Application** をクリックします
        3. ポップアップウィンドウでアプリの名前を入力し、アプリのタイプとして `Single Page Web Application` を選択して **Create** をクリックします
        4. Application Details ページの **Settings** タブに切り替えます
        5. `.env` ファイル内の `YOUR_AUTH0_APP_DOMAIN` と `YOUR_AUTH0_APP_CLIENT_ID` を、ダッシュボードの **Domain** と **Client ID** の値に置き換えます

        最後に、Application Details ページの **Settings** タブで次の URL を設定します。

        **Allowed Callback URLs:**

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

        **許可されたログアウトURL：**

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

        **許可されている Web オリジン:**

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

        <Info>
          **Allowed Callback URLs** は、認証後にユーザーを安全にご利用のアプリケーションに戻すための重要なセキュリティ対策です。一致する URL が設定されていない場合、ログイン処理は失敗し、ユーザーはアプリにアクセスする代わりに Auth0 のエラーページでブロックされます。

          **Allowed Logout URLs** は、サインアウト時にシームレスなユーザー体験を提供するために不可欠です。一致する URL が設定されていない場合、ユーザーはログアウト後にアプリケーションへリダイレクトされず、汎用の Auth0 ページに留まることになります。

          **Allowed Web Origins** は、サイレント認証のために極めて重要です。これが設定されていない場合、ユーザーはページを更新したり、後でアプリに戻ったりするとログアウトされてしまいます。
        </Info>

        **必須: Auth0 API を作成する**

        トークン検証を行うには、Auth0 API を作成する必要があります:

        1. Auth0 Dashboard で **APIs** > **Create API** に進みます
        2. 次の値を設定します:
           * **Name**: `Cap'n Web API`
           * **Identifier**: `https://capnweb-api.yourproject.com`（独自の一意な識別子を使用）
           * **Signing Algorithm**: `RS256`（デフォルト）
        3. **Create** をクリックします
        4. **Scopes** タブで、次のスコープを追加します:
           * `read:profile` - ユーザーのプロファイルデータを読み取る
           * `write:profile` - ユーザーのプロファイルデータを更新する
        5. `.env` ファイルを更新し、API 識別子を `AUTH0_AUDIENCE` として設定します

        <Warning>
          Auth0 API を作成しない場合、ログイン中に "access\_denied" エラーが発生します。API 識別子は、`AUTH0_AUDIENCE` 環境変数と完全に一致している必要があります。
        </Warning>
      </Tab>
    </Tabs>
  </Step>

  <Step title="サーバーの作成" stepNumber={5}>
    Auth0統合を使用してCap'n Webサーバーを作成します：

    ```javascript server/index.js expandable lines theme={null}
    import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
    import { WebSocketServer } from 'ws';
    import { ApiClient } from '@auth0/auth0-api-js';
    import http from 'http';
    import { readFileSync } from 'fs';
    import { dirname, join } from 'path';
    import { fileURLToPath } from 'url';
    import dotenv from 'dotenv';

    dotenv.config();

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

    // 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('❌ 必須のAuth0環境変数が不足しています');
      if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAINが必須です');
      if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_IDが必須です');
      if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCEが必須です');
      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(`トークンの検証に失敗しました: ${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 || '不明なユーザー',
          bio: profile.bio
        };
      }

      async updateProfile(accessToken, bio) {
        const decoded = await verifyToken(accessToken);
        const userId = decoded.sub;
        userProfiles.set(userId, { bio });
        
        return { success: true, message: 'プロファイルが正常に更新されました' };
      }
    }

    // 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('見つかりません');
    });

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

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

    // サーバーを起動
    server.listen(PORT, () => {
      console.log(`🚀 Cap'n Web Auth0サーバーが起動しました`);
      console.log(`📍 サーバーはhttp://localhost:${PORT}で実行中`);
      console.log(`🔐 Auth0ドメイン: ${AUTH0_DOMAIN}`);
      console.log(`🆔 クライアントID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
      console.log(`🎯 APIオーディエンス: ${AUTH0_AUDIENCE}`);
    });
    ```
  </Step>

  <Step title="クライアントインターフェイスを作成する" stepNumber={6}>
    フロントエンドのHTMLとJavaScriptファイルを作成します:

    <AuthCodeGroup>
      ```html client/index.html expandable lines theme={null}
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Cap'n Web + Auth0 デモ</title>
          <script type="importmap">
          {
            "imports": {
              "@auth0/auth0-spa-js": "/@auth0/auth0-spa-js",
              "capnweb": "/capnweb"
            }
          }
          </script>
          <style>
              body {
                  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                  max-width: 600px;
                  margin: 0 auto;
                  padding: 2rem;
                  line-height: 1.6;
              }
              .auth-section, .profile-section {
                  background: #f8f9fa;
                  border-radius: 8px;
                  padding: 2rem;
                  margin: 1rem 0;
              }
              .profile-section { display: none; }
              button {
                  background: #007bff;
                  color: white;
                  border: none;
                  padding: 0.75rem 1.5rem;
                  border-radius: 4px;
                  cursor: pointer;
                  font-size: 1rem;
                  margin: 0.5rem 0.5rem 0.5rem 0;
              }
              button:hover { background: #0056b3; }
              button.secondary { background: #6c757d; }
              button.secondary:hover { background: #545b62; }
              textarea {
                  width: 100%;
                  min-height: 100px;
                  margin: 1rem 0;
                  padding: 0.75rem;
                  border: 1px solid #ddd;
                  border-radius: 4px;
                  resize: vertical;
              }
              .status {
                  padding: 1rem;
                  margin: 1rem 0;
                  border-radius: 4px;
                  font-weight: 500;
              }
              .status.success { background: #d4edda; color: #155724; }
              .status.error { background: #f8d7da; color: #721c24; }
              .status.info { background: #d1ecf1; color: #0c5460; }
          </style>
      </head>
      <body>
          <h1>Cap'n Web + Auth0 プロファイルデモ</h1>
          
          <div id="status" class="status info">初期化中...</div>
          
          <!-- Login Section -->
          <div id="authSection" class="auth-section">
              <h2>🔐 ようこそ!</h2>
              <p>このデモでは、Cap'n Web RPCを使用したAuth0認証によるリアルタイムプロファイル管理を紹介します。</p>
              <button id="loginBtn">Auth0でログイン</button>
          </div>
          
          <!-- Profile Section -->
          <div id="profileSection" class="profile-section">
              <h2>👤 プロファイル管理</h2>
              <p><strong>メール:</strong> <span id="userEmail">読み込み中...</span></p>
              
              <label for="bioTextarea"><strong>自己紹介:</strong></label>
              <textarea id="bioTextarea" placeholder="自己紹介を入力してください..."></textarea>
              
              <div>
                  <button id="fetchBtn">🔄 プロファイルを取得</button>
                  <button id="saveBtn">💾 プロファイルを保存</button>
                  <button id="logoutBtn" class="secondary">🚪 ログアウト</button>
              </div>
          </div>

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

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

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

      // Auth0設定 - サーバーから動的に読み込み
      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('設定を読み込んでいます...', 'info');
          
          const config = await loadConfig();
          
          showStatus('Auth0クライアントを初期化しています...', 'info');
          auth0Client = await createAuth0Client(config);
          
          const query = window.location.search;
          if (query.includes('code=') && query.includes('state=')) {
            showStatus('ログイン処理中...', 'info');
            await auth0Client.handleRedirectCallback();
            window.history.replaceState({}, document.title, window.location.pathname);
          }
          
          const isAuthenticated = await auth0Client.isAuthenticated();
          
          if (isAuthenticated) {
            showStatus('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('初期化エラー:', error);
          showStatus(`初期化に失敗しました: ${error.message}`, 'error');
        }
      }

      // 認証関数
      async function login() {
        try {
          showStatus('Auth0にリダイレクト中...', 'info');
          await auth0Client.loginWithRedirect();
        } catch (error) {
          showStatus(`ログインに失敗しました: ${error.message}`, 'error');
        }
      }

      async function logout() {
        try {
          if (profileApi) {
            profileApi[Symbol.dispose]();
          }
          await auth0Client.logout({
            logoutParams: { returnTo: window.location.origin }
          });
        } catch (error) {
          showStatus(`ログアウトに失敗しました: ${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;
        }
      }

      // プロファイル管理関数
      async function fetchProfile() {
        try {
          showStatus('プロファイルを取得中...', 'info');
          
          const token = await getAccessToken();
          const user = await auth0Client.getUser();
          const profile = await profileApi.getProfile(token);
          
          document.getElementById('userEmail').textContent = user.email || profile.email || 'メールアドレスがありません';
          document.getElementById('bioTextarea').value = profile.bio || '';
          
          showStatus('プロファイルを読み込みました', 'success');
        } catch (error) {
          showStatus(`プロファイルの取得に失敗しました: ${error.message}`, 'error');
        }
      }

      async function saveProfile() {
        try {
          showStatus('プロファイルを保存中...', 'info');
          
          const token = await getAccessToken();
          const bio = document.getElementById('bioTextarea').value;
          
          const result = await profileApi.updateProfile(token, bio);
          
          showStatus(result.message || 'プロファイルを保存しました', 'success');
        } catch (error) {
          showStatus(`プロファイルの保存に失敗しました: ${error.message}`, 'error');
        }
      }

      // UIヘルパー関数
      function showAuthSection() {
        document.getElementById('authSection').style.display = 'block';
        document.getElementById('profileSection').style.display = 'none';
        showStatus('ログイン可能', '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);
      ```
    </AuthCodeGroup>
  </Step>

  <Step title="アプリを実行する" stepNumber={7}>
    ```shellscript theme={null}
    npm run start
    ```
  </Step>
</Steps>

<Check>
  **チェックポイント**

  これで、[localhost](http://localhost:3000/) 上で Auth0 のログインページが問題なく動作しているはずです。
</Check>

***

<div id="advanced-usage">
  ## 高度な利用方法
</div>

<Accordion title="サーバーサイド RPC セキュリティ">
  RPC メソッドに追加の検証やレート制限を加えて、セキュリティを強化します。

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

<Accordion title="WebSocket 接続管理">
  自動再接続機能付きで、WebSocket 接続を適切に処理します。

  ```javascript client/connection-manager.js theme={null}
  import { newWebSocketRpcSession } from 'capnweb';

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

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

      try {
        // 既存の接続があれば破棄する
        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();
  }
  ```
</Accordion>

<Accordion title="データベース統合">
  本番環境ではインメモリストレージをデータベースに置き換えてください:

  ```javascript server/database.js theme={null}
  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: 'プロファイルが正常に更新されました' };
    }
  }

  export { initializeDatabase, DatabaseProfileService };
  ```
</Accordion>
