diff --git a/03-Capn-Web/.env.example b/03-Capn-Web/.env.example new file mode 100644 index 0000000..666b55b --- /dev/null +++ b/03-Capn-Web/.env.example @@ -0,0 +1,26 @@ +# Cap'n Web + Auth0 Demo Configuration +# IMPORTANT: Copy this file to .env and update with your actual values + +# Server Configuration +PORT=3000 +HOST=localhost +NODE_ENV=development + +# Auth0 Configuration +# Get these values from your Auth0 Dashboard > Applications > [Your App] +AUTH0_DOMAIN=your-domain.us.auth0.com +AUTH0_AUDIENCE=https://api.your-app.com + +# Auth0 Client Configuration (for the web app) +# Get this from Auth0 Dashboard > Applications > [Your App] > Settings +AUTH0_CLIENT_ID=your-auth0-client-id + +# Optional: Add artificial delays for testing (in milliseconds) +DELAY_PROFILE_MS=100 + +# Optional: Database Configuration (for production) +# DATABASE_URL=postgresql://username:password@localhost:5432/capnweb_demo + +# Optional: Redis Configuration (for session storage) +# REDIS_URL=redis://localhost:6379 +# REDIS_URL=redis://localhost:6379 \ No newline at end of file diff --git a/03-Capn-Web/.gitignore b/03-Capn-Web/.gitignore new file mode 100644 index 0000000..27fa5c6 --- /dev/null +++ b/03-Capn-Web/.gitignore @@ -0,0 +1,96 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Environment variables (NEVER commit these!) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Logs +logs +*.log + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# IDE and editor files +.vscode/settings.json +.vscode/launch.json +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Build outputs +dist/ +build/ +out/ + +# Temporary files +*.tmp +*.temp + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/03-Capn-Web/CONTRIBUTING.md b/03-Capn-Web/CONTRIBUTING.md new file mode 100644 index 0000000..ded81bf --- /dev/null +++ b/03-Capn-Web/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to Cap'n Web + Auth0 Demo + +Thank you for your interest in contributing to this project! This demo showcases the integration between Cap'n Web RPC and Auth0 authentication. + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/capn-web-auth0-demo.git` +3. Install dependencies: `npm install` +4. Set up environment: `npm run setup` +5. Update `.env` with your Auth0 configuration +6. Start the development server: `npm run dev` + +## Development Guidelines + +### Code Style +- Use consistent indentation (2 spaces) +- Follow existing naming conventions +- Add comments for complex logic +- Maintain the existing dark theme design patterns + +### Security +- Never commit credentials or API keys +- All authentication logic should be server-side validated +- Follow object-capability security patterns +- Test all authentication flows thoroughly + +### Documentation +- Update README.md for any new features +- Add inline comments for complex RPC interactions +- Update the environment configuration examples + +## Pull Request Process + +1. Create a feature branch: `git checkout -b feature/your-feature-name` +2. Make your changes +3. Test thoroughly with different Auth0 configurations +4. Update documentation if needed +5. Submit a pull request with a clear description + +## Testing + +Before submitting a PR: +- Test the authentication flow end-to-end +- Verify WebSocket RPC calls work correctly +- Check that the pipelining demo functions properly +- Test with both development and production-like environments + +## Questions? + +Feel free to open an issue for questions about: +- Cap'n Web RPC implementation +- Auth0 integration patterns +- Project structure and architecture +- Development setup issues + +Thank you for contributing! ๐Ÿš€ \ No newline at end of file diff --git a/03-Capn-Web/Dockerfile b/03-Capn-Web/Dockerfile new file mode 100644 index 0000000..cb39f83 --- /dev/null +++ b/03-Capn-Web/Dockerfile @@ -0,0 +1,29 @@ +# Cap'n Web + Auth0 Demo Dockerfile +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application code +COPY . . + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs +RUN adduser -S capnweb -u 1001 +USER capnweb + +# Expose port +EXPOSE 3000 + +# Add health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" + +# Start application +CMD ["npm", "start"] \ No newline at end of file diff --git a/03-Capn-Web/README.md b/03-Capn-Web/README.md new file mode 100644 index 0000000..dc96015 --- /dev/null +++ b/03-Capn-Web/README.md @@ -0,0 +1,301 @@ +# Cap'n Web + Auth0 Demo: User Profile Management + +> **A production-ready sample application showcasing secure user profile management using Cap'n Web RPC and Auth0 authentication** + +## ๐ŸŽฏ What This Demo Shows + +This application demonstrates how to build a **user profile management system** that combines: + +- **๐Ÿ” Auth0 Authentication**: Users log in securely using Auth0's hosted login page +- **โšก Cap'n Web RPC**: Real-time API calls for profile operations via WebSocket RPC +- **๐Ÿ‘ค Profile Management**: Users can view and edit their personal profile information +- **๐Ÿ›ก๏ธ JWT Authorization**: Auth0 JWT tokens authorize all API calls to ensure users only access their own data + +### Use Case: Personal Profile Dashboard + +**The Story**: Users need a secure way to manage their profile information. Auth0 handles the complex authentication flow, while Cap'n Web provides fast, real-time API communication for profile updates. + +**User Journey**: +1. **Landing Page** โ†’ User clicks "Authenticate with Auth0" +2. **Auth0 Login** โ†’ Secure authentication via Auth0's hosted login +3. **Profile Dashboard** โ†’ User sees their email and can edit their bio +4. **Real-time Updates** โ†’ Profile changes are saved instantly via WebSocket RPC +5. **Data Security** โ†’ JWT tokens ensure users only see/edit their own profiles + +**Technical Flow**: +- Auth0 provides secure user authentication and JWT tokens +- Cap'n Web RPC handles profile API calls (get/update profile) +- Server validates JWT tokens and isolates user data +- In-memory storage keeps the demo simple while showing the patterns + +This demo application demonstrates how to build secure, real-time applications using [Cap'n Web RPC](https://github.com/cloudflare/capnweb) with [Auth0](https://github.com/auth0/auth0-spa-js) authentication. It features object-capability security, WebSocket-based RPC communication, and JWT token verification. + +![Auth0 + Cap'n Web Demo](https://img.shields.io/badge/Auth0-Integration-orange) ![Cap'n Web RPC](https://img.shields.io/badge/Cap'n%20Web-RPC-blue) ![Node.js](https://img.shields.io/badge/Node.js-18+-green) ![License](https://img.shields.io/badge/License-MIT-yellow) + +## ๐Ÿš€ Features + +- **๐Ÿ” Secure Authentication**: Auth0 handles user login with industry-standard security +- **๐Ÿ‘ค User Profile Management**: View and edit personal profile information (email, bio) +- **โšก Real-time RPC**: Cap'n Web provides fast WebSocket-based API communication +- **๐Ÿ›ก๏ธ JWT Authorization**: Auth0 tokens authorize all API calls to protect user data +- **๐ŸŽจ Modern UI**: Clean, responsive dark theme with Auth0 branding +- **๐Ÿ”„ Live Updates**: Profile changes saved instantly without page refreshes +- **๏ฟฝ Production Ready**: Environment-based configuration, Docker support, comprehensive documentation +- **๐Ÿงน Developer Friendly**: Easy setup, clear error handling, debugging tools included + +## ๐Ÿ“ Project Structure + +``` +capn-web-auth0-demo/ +โ”œโ”€โ”€ client/ # Frontend application +โ”‚ โ”œโ”€โ”€ index.html # Main UI with Auth0 branding & dark theme +โ”‚ โ”œโ”€โ”€ client.js # Auth0 integration & Cap'n Web RPC client +โ”‚ โ””โ”€โ”€ capnweb-browser.js # Browser-compatible Cap'n Web implementation +โ”œโ”€โ”€ server/ # Backend application +โ”‚ โ””โ”€โ”€ index.js # WebSocket RPC server with JWT validation +โ”œโ”€โ”€ scripts/ # Maintenance and setup scripts +โ”‚ โ””โ”€โ”€ cleanup.sh # Repository validation script +โ”œโ”€โ”€ .env.example # Environment configuration template +โ”œโ”€โ”€ .gitignore # Git ignore patterns +โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines +โ”œโ”€โ”€ Dockerfile # Container configuration +โ”œโ”€โ”€ docker-compose.yml # Multi-container development setup +โ””โ”€โ”€ package.json # Dependencies, scripts, and metadata +``` + +## โšก Quick Start + +1. **Clone and install dependencies:** + ```bash + git clone https://github.com/auth0-samples/auth0-javascript-samples.git + cd 03-Capn-Web + npm install + ``` + +2. **Set up environment:** + ```bash + npm run setup + # Then edit .env with your Auth0 configuration + ``` + +3. **Start the application:** + ```bash + npm run dev + ``` + +4. **Open your browser:** + ``` + http://localhost:3000 + ``` + +## ๐Ÿ—๏ธ Architecture + +### Server Side (Node.js) +- **ProfileService**: Cap'n Web RPC target with two methods: + - `getProfile()`: Returns user email and bio based on Auth0 token + - `updateProfile(newBio)`: Updates user bio with Auth0 token validation +- **Auth0 Integration**: JWT token verification with JWKS +- **In-memory Storage**: User profiles stored by Auth0 user ID + +### Client Side (Vanilla JavaScript) +- **Auth0 SPA SDK**: Handles login/logout flow +- **Cap'n Web Client**: WebSocket RPC communication +- **Profile UI**: Bio editing with save/fetch functionality + +## ๐Ÿ“‹ Prerequisites + +- **Node.js 18+** (LTS recommended) +- **Auth0 account** - Free tier available at [auth0.com](https://auth0.com) +- **Modern web browser** with WebSocket support +- **Git** for cloning the repository + +## โš™๏ธ Setup + +### 1. Auth0 Configuration + +1. Create an Auth0 account at [auth0.com](https://auth0.com) +2. Create a new Single Page Application +3. Configure the following settings: + - **Allowed Callback URLs**: `http://localhost:3000` + - **Allowed Logout URLs**: `http://localhost:3000` + - **Allowed Web Origins**: `http://localhost:3000` +4. Note down your: + - Domain (e.g., `your-domain.us.auth0.com`) + - Client ID + - API Audience (if using Auth0 APIs) + +### 2. Environment Configuration + +โš ๏ธ **IMPORTANT**: Never commit credentials to your repository! + +1. Copy the environment template: + ```bash + cp .env.example .env + ``` + +2. Update `.env` with your Auth0 configuration: + ```bash + # Auth0 Configuration + AUTH0_DOMAIN=your-domain.us.auth0.com + AUTH0_CLIENT_ID=your-auth0-client-id + AUTH0_AUDIENCE=https://api.your-app.com + + # Server Configuration + PORT=3000 + NODE_ENV=development + ``` + +3. The application will automatically load these values - no code changes needed! + +### 3. Install Dependencies + +```bash +npm install +``` + +## ๐Ÿš€ Running the Application + +1. Start the server: + +```bash +npm run dev +``` + +2. Open your browser and navigate to: +``` +http://localhost:3000 +``` + +## ๐Ÿ”„ User Flow + +1. **Landing Page**: User sees a "Log In" button +2. **Auth0 Login**: Clicking login redirects to Auth0 authentication +3. **Authenticated State**: After login, user sees: + - Their email address (from Auth0 token) + - A text area for bio editing + - Save, Fetch, and Logout buttons +4. **Profile Management**: + - **Save**: Updates bio via Cap'n Web RPC call + - **Fetch**: Retrieves saved bio from server + - **Logout**: Clears session and returns to login screen + +## ๐Ÿ”ง Technical Details + +### Cap'n Web RPC Flow + +1. Client establishes WebSocket connection to `/api` +2. Auth0 token is sent for authentication +3. Server creates `ProfileService` instance with user token +4. RPC calls are made through Cap'n Web protocol +5. Server validates token on each call and isolates data by user ID + +### Security Features + +- **Token Validation**: All RPC calls validate Auth0 JWT tokens +- **User Isolation**: Profiles are stored and retrieved by Auth0 user ID +- **Object Capabilities**: Cap'n Web's object-capability model ensures secure RPC + +### API Methods + +#### `getProfile()` +- **Returns**: `{ email: string, bio: string }` +- **Security**: Validates Auth0 token and returns user-specific data + +#### `updateProfile(newBio)` +- **Parameters**: `newBio: string` +- **Returns**: `{ success: boolean, message: string }` +- **Security**: Validates Auth0 token and updates user-specific data + +## ๏ฟฝ Docker Support + +Run the application using Docker for consistent development environments: + +### Using Docker Compose (Recommended) +```bash +# Start with docker-compose +npm run docker:dev + +# Or manually +docker-compose up --build +``` + +### Using Docker directly +```bash +# Build the image +npm run docker:build + +# Run with environment file +npm run docker:run +``` + +The Docker setup includes: +- Node.js 18 Alpine base image +- Non-root user for security +- Health checks for monitoring +- Volume mounts for development + +## ๐Ÿ”— Key Dependencies + +- **capnweb**: RPC library for browser-server communication +- **dotenv**: Environment variable management +- **jsonwebtoken**: JWT token verification +- **jwks-client**: Auth0 JWKS key retrieval +- **ws**: WebSocket server implementation + +## ๐Ÿค Contributing + +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for: + +- Development setup guidelines +- Code style requirements +- Security considerations +- Pull request process + +For questions or issues, please: +1. Check existing [issues](https://github.com/YOUR_USERNAME/capn-web-auth0-demo/issues) +2. Review the [troubleshooting section](#-troubleshooting) +3. Open a new issue with detailed information + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Auth0 Configuration**: Ensure domain and client ID are correct +2. **CORS Issues**: Check that callback URLs are properly configured +3. **WebSocket Connection**: Verify server is running on correct port +4. **Token Validation**: Check Auth0 domain and audience configuration + +### Development Tips + +- Check browser console for detailed error messages +- Monitor server logs for authentication and RPC call details +- Use browser DevTools to inspect WebSocket traffic +- Verify Auth0 token in [jwt.io](https://jwt.io) for debugging +- Run `npm run cleanup` to validate repository setup + +## ๐Ÿ“š Learn More + +- [Cap'n Web Documentation](https://github.com/cloudflare/capnweb) +- [Auth0 SPA SDK Guide](https://auth0.com/docs/libraries/auth0-spa-js) +- [Cap'n Web Blog Post](https://blog.cloudflare.com/capnweb-javascript-rpc-library/) + +## ๏ฟฝ What You'll Learn + +This demo teaches you how to: + +- **Integrate Auth0** for secure user authentication in web applications +- **Use Cap'n Web RPC** for real-time, WebSocket-based API communication +- **Handle JWT tokens** for API authorization and user data protection +- **Build responsive UIs** with modern web technologies and dark themes +- **Structure projects** for production deployment with Docker and environment management +- **Implement user data isolation** ensuring users can only access their own profiles + +Perfect for developers exploring modern authentication patterns, real-time communication, or looking for a foundation to build user-centric applications. + +## ๐Ÿ“„ License + +MIT License - see [LICENSE](LICENSE) file for details. + +--- + +**Built with โค๏ธ for developers exploring Auth0 and Cap'n Web integration** \ No newline at end of file diff --git a/03-Capn-Web/client/capnweb-browser.js b/03-Capn-Web/client/capnweb-browser.js new file mode 100644 index 0000000..fc9f991 --- /dev/null +++ b/03-Capn-Web/client/capnweb-browser.js @@ -0,0 +1,151 @@ +// Simplified Cap'n Web browser implementation for demonstration +// In production, you would use a proper bundler like Vite, Webpack, or Rollup + +// Simple polyfill for Symbol.dispose if not supported +if (!Symbol.dispose) { + Symbol.dispose = Symbol.for('dispose'); +} + +// Basic RPC Target implementation +class RpcTarget { + constructor() { + // Mark as RPC target + this.__isRpcTarget = true; + } +} + +// Simple RPC session implementation for WebSocket +class SimpleRpcSession { + constructor(webSocket) { + this.webSocket = webSocket; + this.callId = 0; + this.pendingCalls = new Map(); + this.isReady = false; + this.readyPromise = null; + + // Wait for WebSocket to open + if (webSocket.readyState === WebSocket.OPEN) { + this.isReady = true; + this.readyPromise = Promise.resolve(); + } else { + this.readyPromise = new Promise((resolve, reject) => { + const onOpen = () => { + this.isReady = true; + webSocket.removeEventListener('open', onOpen); + webSocket.removeEventListener('error', onError); + resolve(); + }; + + const onError = (error) => { + webSocket.removeEventListener('open', onOpen); + webSocket.removeEventListener('error', onError); + reject(error); + }; + + webSocket.addEventListener('open', onOpen); + webSocket.addEventListener('error', onError); + }); + } + + this.webSocket.addEventListener('message', (event) => { + this.handleMessage(event.data); + }); + } + + handleMessage(data) { + try { + const message = JSON.parse(data); + + if (message.type === 'response' && this.pendingCalls.has(message.id)) { + const { resolve, reject } = this.pendingCalls.get(message.id); + this.pendingCalls.delete(message.id); + + if (message.error) { + reject(new Error(message.error)); + } else { + resolve(message.result); + } + } + } catch (error) { + console.error('Error handling message:', error); + } + } + + async call(method, params = []) { + // Wait for WebSocket to be ready + await this.readyPromise; + + return new Promise((resolve, reject) => { + const id = ++this.callId; + + this.pendingCalls.set(id, { resolve, reject }); + + const message = { + type: 'call', + id: id, + method: method, + params: params + }; + + try { + this.webSocket.send(JSON.stringify(message)); + } catch (error) { + this.pendingCalls.delete(id); + reject(error); + return; + } + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingCalls.has(id)) { + this.pendingCalls.delete(id); + reject(new Error('Request timeout')); + } + }, 30000); + }); + } + + [Symbol.dispose]() { + this.webSocket.close(); + } +} + +// Create a proxy that intercepts method calls and routes them through RPC +function createRpcProxy(session) { + return new Proxy({}, { + get(target, prop) { + if (prop === Symbol.dispose) { + return () => session[Symbol.dispose](); + } + + // Don't intercept promise methods or private properties + if (typeof prop === 'string' && + !prop.startsWith('_') && + !['then', 'catch', 'finally', 'constructor', 'toString', 'valueOf'].includes(prop)) { + return (...args) => session.call(prop, args); + } + + return target[prop]; + } + }); +} + +// Simple WebSocket RPC session factory +async function newWebSocketRpcSession(webSocketOrUrl) { + const webSocket = typeof webSocketOrUrl === 'string' + ? new WebSocket(webSocketOrUrl) + : webSocketOrUrl; + + const session = new SimpleRpcSession(webSocket); + + // Wait for the WebSocket to be ready + await session.readyPromise; + + return createRpcProxy(session); +} + +// Export for use in the application +window.CapnWeb = { + RpcTarget, + newWebSocketRpcSession +}; \ No newline at end of file diff --git a/03-Capn-Web/client/client.js b/03-Capn-Web/client/client.js new file mode 100644 index 0000000..3ad6564 --- /dev/null +++ b/03-Capn-Web/client/client.js @@ -0,0 +1,367 @@ +// Auth0 Configuration - Loaded dynamically from server +let AUTH0_CONFIG = null; + +// Load configuration from server +async function loadConfig() { + try { + const response = await fetch('/api/config'); + if (!response.ok) { + throw new Error(`Failed to load configuration: ${response.status}`); + } + 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 + } + }; + + console.log('โœ… Configuration loaded successfully'); + return AUTH0_CONFIG; + } catch (error) { + console.error('โŒ Failed to load configuration:', error); + throw error; + } +} + +class ProfileApp { + constructor() { + this.auth0 = null; + this.profileService = null; + this.sessionId = null; + this.initializeApp(); + } + + async initializeApp() { + try { + // Load configuration from server + this.showStatus('Loading configuration...', 'info'); + await loadConfig(); + + if (!AUTH0_CONFIG) { + throw new Error('Failed to load Auth0 configuration'); + } + + // Initialize Auth0 + this.showStatus('Initializing Auth0...', 'info'); + this.auth0 = await auth0.createAuth0Client(AUTH0_CONFIG); + + // Check if user is returning from login + const query = window.location.search; + if (query.includes('code=') && query.includes('state=')) { + this.showStatus('Processing login...', 'info'); + await this.auth0.handleRedirectCallback(); + window.history.replaceState({}, document.title, window.location.pathname); + } + + // Check authentication status + const isAuthenticated = await this.auth0.isAuthenticated(); + + if (isAuthenticated) { + await this.handleAuthenticated(); + } else { + this.showLoginScreen(); + } + + this.setupEventListeners(); + } catch (error) { + console.error('Error initializing app:', error); + this.showStatus('Failed to initialize application', 'error'); + } + } + + setupEventListeners() { + document.getElementById('loginBtn').addEventListener('click', () => this.login()); + document.getElementById('logoutBtn').addEventListener('click', () => this.logout()); + document.getElementById('saveBtn').addEventListener('click', () => this.saveProfile()); + document.getElementById('fetchBtn').addEventListener('click', () => this.fetchProfile()); + document.getElementById('pipelineBtn').addEventListener('click', () => this.demoPipelining()); + } + + async login() { + try { + await this.auth0.loginWithRedirect({ + authorizationParams: { + redirect_uri: window.location.origin, + audience: AUTH0_CONFIG.authorizationParams.audience, + scope: 'openid profile email' + } + }); + } catch (error) { + console.error('Error during login:', error); + this.showStatus('Login failed', 'error'); + } + } + + async logout() { + try { + // Close Cap'n Web connection + if (this.profileService && this.profileService[Symbol.dispose]) { + this.profileService[Symbol.dispose](); + this.profileService = null; + } + + await this.auth0.logout({ + logoutParams: { + returnTo: window.location.origin + } + }); + } catch (error) { + console.error('Error during logout:', error); + } + } + + async handleAuthenticated() { + try { + const user = await this.auth0.getUser(); + + // Show profile section + this.showProfileScreen(user); + + // Initialize Cap'n Web connection + await this.initializeCapnWeb(); + + // Automatically fetch profile + await this.fetchProfile(); + + } catch (error) { + console.error('Error handling authentication:', error); + this.showStatus('Authentication error', 'error'); + } + } + + async initializeCapnWeb() { + try { + console.log('Initializing Cap\'n Web connection...'); + + // Create WebSocket connection + const wsUrl = `ws://${window.location.host}`; + console.log('Connecting to WebSocket:', wsUrl); + + // Use our simple Cap'n Web implementation and wait for connection + this.profileService = await CapnWeb.newWebSocketRpcSession(wsUrl); + + console.log('Cap\'n Web RPC connection established and ready'); + + } catch (error) { + console.error('Error initializing Cap\'n Web:', error); + throw error; + } + } + + async fetchProfile() { + try { + this.setLoading(true); + + if (!this.profileService) { + throw new Error('RPC service not initialized'); + } + + let token; + try { + token = await this.auth0.getTokenSilently({ + authorizationParams: { + audience: AUTH0_CONFIG.authorizationParams.audience + } + }); + } catch (tokenError) { + console.error('Token error:', tokenError); + + // If consent is required, redirect to login with consent + if (tokenError.error === 'consent_required' || tokenError.error === 'interaction_required') { + console.log('Consent required, redirecting to login...'); + await this.auth0.loginWithRedirect({ + authorizationParams: { + audience: AUTH0_CONFIG.authorizationParams.audience, + scope: 'openid profile email', + prompt: 'consent' + } + }); + return; + } + throw tokenError; + } + + console.log('Fetching profile with token...'); + console.log('Token type:', typeof token); + console.log('Token length:', token ? token.length : 'null'); + console.log('Token preview:', token ? token.substring(0, 50) + '...' : 'null'); + + // Call the getProfile method via Cap'n Web RPC + const profile = await this.profileService.getProfile(token); + + document.getElementById('bioTextarea').value = profile.bio || ''; + this.showStatus('Profile loaded successfully', 'success'); + + } catch (error) { + console.error('Error fetching profile:', error); + this.showStatus('Failed to fetch profile: ' + error.message, 'error'); + } finally { + this.setLoading(false); + } + } + + async saveProfile() { + try { + this.setLoading(true); + + if (!this.profileService) { + throw new Error('RPC service not initialized'); + } + + const bio = document.getElementById('bioTextarea').value.trim(); + const token = await this.auth0.getTokenSilently({ + authorizationParams: { + audience: AUTH0_CONFIG.authorizationParams.audience + } + }); + + console.log('Saving profile with bio length:', bio.length); + + // Call the updateProfile method via Cap'n Web RPC + const result = await this.profileService.updateProfile(token, bio); + + if (result.success) { + this.showStatus('Profile saved successfully', 'success'); + } else { + this.showStatus('Failed to save profile', 'error'); + } + + } catch (error) { + console.error('Error saving profile:', error); + this.showStatus('Failed to save profile: ' + error.message, 'error'); + } finally { + this.setLoading(false); + } + } + + async demoPipelining() { + try { + this.setLoading(true); + + if (!this.profileService) { + throw new Error('RPC service not initialized'); + } + + const token = await this.auth0.getTokenSilently({ + authorizationParams: { + audience: AUTH0_CONFIG.authorizationParams.audience + } + }); + + this.showStatus('๐Ÿš€ Demonstrating Cap\'n Web RPC Pipelining...', 'info'); + + // Get current bio from the textarea + const currentBio = document.getElementById('bioTextarea').value.trim(); + const timestamp = new Date().toLocaleTimeString(); + const newBio = `${currentBio} [Pipelined update at ${timestamp}]`; + + console.log('Demo: Starting pipelined operations...'); + console.log('1. Fetching current profile (before update)'); + console.log('2. Updating profile with new bio'); + console.log('3. Both operations running concurrently!'); + + // ๐ŸŽฏ PIPELINING DEMO: Start both operations simultaneously + // This demonstrates Cap'n Web's ability to pipeline multiple RPC calls + const startTime = performance.now(); + + const [oldProfile, updateResult] = await Promise.all([ + // Operation 1: Fetch the current profile (before update) + this.profileService.getProfile(token), + // Operation 2: Update the profile with new bio (simultaneously) + this.profileService.updateProfile(token, newBio) + ]); + + const endTime = performance.now(); + const duration = Math.round(endTime - startTime); + + console.log(`Pipelining completed in ${duration}ms`); + console.log('Old profile bio:', oldProfile.bio); + console.log('Update result:', updateResult); + + // Show the results in a nice format + const oldBioPreview = oldProfile.bio ? + (oldProfile.bio.length > 50 ? oldProfile.bio.substring(0, 50) + '...' : oldProfile.bio) : + '(empty)'; + + this.showStatus( + `โœจ Pipelining Demo Complete! ` + + `Fetched old bio: "${oldBioPreview}" & ` + + `updated profile simultaneously in ${duration}ms`, + 'success' + ); + + // Wait a moment, then fetch the updated profile to show the change + setTimeout(async () => { + await this.fetchProfile(); + this.showStatus( + `๐Ÿ”„ Profile refreshed! Notice how the bio now includes the pipelined update. ` + + `This demonstrates real-time RPC capability.`, + 'info' + ); + }, 2000); + + } catch (error) { + console.error('Error in pipelining demo:', error); + this.showStatus('โŒ Pipelining demo failed: ' + error.message, 'error'); + } finally { + this.setLoading(false); + } + } + + showLoginScreen() { + const authSection = document.getElementById('authSection'); + const profileSection = document.getElementById('profileSection'); + + if (authSection) authSection.style.display = 'block'; + if (profileSection) profileSection.style.display = 'none'; + } + + showProfileScreen(user) { + const authSection = document.getElementById('authSection'); + const profileSection = document.getElementById('profileSection'); + const userEmailEl = document.getElementById('userEmail'); + + if (authSection) authSection.style.display = 'none'; + if (profileSection) profileSection.style.display = 'block'; + if (userEmailEl) userEmailEl.textContent = user.email || user.name || 'Unknown User'; + } + + setLoading(loading) { + const container = document.querySelector('.main-container'); + if (container) { + if (loading) { + container.classList.add('loading'); + } else { + container.classList.remove('loading'); + } + } + } + + showStatus(message, type) { + const statusEl = document.getElementById('status'); + if (!statusEl) { + console.warn('Status element not found'); + return; + } + + statusEl.innerHTML = message; // Use innerHTML to support icons/HTML + statusEl.className = `status ${type}`; + statusEl.style.display = 'block'; + + // Auto-hide success/info messages after 5 seconds, keep errors visible + if (type === 'success' || type === 'info') { + setTimeout(() => { + if (statusEl) statusEl.style.display = 'none'; + }, 5000); + } + } +} + +// Initialize the app when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + new ProfileApp(); +}); \ No newline at end of file diff --git a/03-Capn-Web/client/index.html b/03-Capn-Web/client/index.html new file mode 100644 index 0000000..fef4cdf --- /dev/null +++ b/03-Capn-Web/client/index.html @@ -0,0 +1,589 @@ + + + + + + Cap'n Web + Auth0 Demo + + + + + + + + + +
+
+
+
+ + +
+

Object-Capability RPC with Secure Authentication

+ Developer Demo +
+
+ +
+
+

Welcome, Developer! ๐Ÿ‘‹

+

This demo showcases Cap'n Web RPC integration with Auth0 authentication. + Experience secure, real-time profile management with object-capability security.

+ +
+ +
+
+
+ +
+ +
+ +
+

+ + Profile Management +

+
+ + +
+
+ +
+

RPC Pipelining Demo

+

The "Demo Pipelining" button showcases Cap'n Web's ability to execute multiple RPC calls concurrently, + demonstrating how you can save a profile and fetch the old profile data simultaneously for better performance.

+
+ +
+ + + + +
+ +
+
+
+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/03-Capn-Web/docker-compose.yml b/03-Capn-Web/docker-compose.yml new file mode 100644 index 0000000..582e61d --- /dev/null +++ b/03-Capn-Web/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + capn-web-demo: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - AUTH0_DOMAIN=${AUTH0_DOMAIN} + - AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} + - AUTH0_AUDIENCE=${AUTH0_AUDIENCE} + volumes: + - ./server:/app/server:ro + - ./client:/app/client:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/03-Capn-Web/package-lock.json b/03-Capn-Web/package-lock.json new file mode 100644 index 0000000..e7b163b --- /dev/null +++ b/03-Capn-Web/package-lock.json @@ -0,0 +1,149 @@ +{ + "name": "capn-web-auth0-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "capn-web-auth0-demo", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@auth0/auth0-api-js": "^1.0.0", + "capnweb": "^0.1.0", + "dotenv": "^17.2.2", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@auth0/auth0-api-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-api-js/-/auth0-api-js-1.1.0.tgz", + "integrity": "sha512-i7NDLyDlOkxC9QEXHVxxQt3KoUdKlRfijsa/eitsAVFnQNOYuWhBNtGDYMSQ5enWnTtyMPOCAXUOCLfoLB07Eg==", + "license": "MIT", + "dependencies": { + "@auth0/auth0-auth-js": "^1.1.0", + "jose": "^6.0.8", + "oauth4webapi": "^3.3.0" + } + }, + "node_modules/@auth0/auth0-auth-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-auth-js/-/auth0-auth-js-1.1.0.tgz", + "integrity": "sha512-AGv60cY8/maMa2DQooa+MfJjSOYuHL0mOOIC9FbyqnMt0k4Vo08qpsNLlAHtWTK2VrAZZYP20jnZS6Ds/Apz/Q==", + "license": "MIT", + "dependencies": { + "jose": "^6.0.8", + "openid-client": "^6.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/capnweb": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/capnweb/-/capnweb-0.1.0.tgz", + "integrity": "sha512-+pygKx1JFTZTRdd1hHgaBRg5BwULEDZq8ZoHXkYP2GXNV3lrjXLj5qzlGz+SgBCJjWUmNBtlh7JPWdr0wIbY8w==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.2.tgz", + "integrity": "sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz", + "integrity": "sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==", + "license": "MIT", + "dependencies": { + "jose": "^6.1.0", + "oauth4webapi": "^3.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/03-Capn-Web/package.json b/03-Capn-Web/package.json new file mode 100644 index 0000000..b98f05f --- /dev/null +++ b/03-Capn-Web/package.json @@ -0,0 +1,57 @@ +{ + "name": "capn-web-auth0-demo", + "version": "1.0.0", + "description": "Production-ready Cap'n Web RPC demo with Auth0 authentication - showcasing modern RPC patterns for developers", + "main": "server/index.js", + "type": "module", + "scripts": { + "dev": "node server/index.js", + "start": "node server/index.js", + "setup": "cp .env.example .env && echo 'Please update .env with your Auth0 configuration'", + "cleanup": "bash scripts/cleanup.sh", + "test": "echo \"Add your tests here\" && exit 0", + "lint": "echo \"Add linting here\" && exit 0", + "docker:build": "docker build -t capn-web-auth0-demo .", + "docker:run": "docker run -p 3000:3000 --env-file .env capn-web-auth0-demo", + "docker:dev": "docker-compose up --build" + }, + "repository": { + "type": "git", + "url": "https://github.com/YOUR_USERNAME/capn-web-auth0-demo.git" + }, + "keywords": [ + "capnweb", + "auth0", + "rpc", + "websocket", + "nodejs", + "javascript", + "authentication", + "real-time", + "demo", + "sample-app", + "developer-tools", + "object-capabilities", + "pipelining" + ], + "author": "Auth0 Community ", + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "dependencies": { + "capnweb": "^0.1.0", + "dotenv": "^17.2.2", + "@auth0/auth0-api-js": "^1.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "bugs": { + "url": "https://github.com/YOUR_USERNAME/capn-web-auth0-demo/issues" + }, + "homepage": "https://github.com/YOUR_USERNAME/capn-web-auth0-demo#readme" +} diff --git a/03-Capn-Web/server/index.js b/03-Capn-Web/server/index.js new file mode 100644 index 0000000..7af6079 --- /dev/null +++ b/03-Capn-Web/server/index.js @@ -0,0 +1,292 @@ +import { RpcTarget } from 'capnweb'; +import { WebSocketServer } from 'ws'; +import { ApiClient } from '@auth0/auth0-api-js'; +import http from 'http'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// In-memory user profile store +const userProfiles = new Map(); + +// Auth0 configuration from environment variables +const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN; +const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE; + +// Validate required environment variables +if (!AUTH0_DOMAIN || !AUTH0_AUDIENCE) { + console.error('โŒ Missing required environment variables:'); + if (!AUTH0_DOMAIN) console.error(' - AUTH0_DOMAIN is required'); + if (!AUTH0_AUDIENCE) console.error(' - AUTH0_AUDIENCE is required'); + console.error('๐Ÿ“ Please copy .env.example to .env and update with your Auth0 configuration'); + process.exit(1); +} + +// Initialize Auth0 API client for token verification +const auth0ApiClient = new ApiClient({ + domain: AUTH0_DOMAIN, + audience: AUTH0_AUDIENCE +}); + +// Verify Auth0 JWT/JWE token using the official Auth0 API client +async function verifyToken(token) { + try { + const payload = await auth0ApiClient.verifyAccessToken({ + accessToken: token + }); + return payload; + } catch (error) { + throw new Error(`Token verification failed: ${error.message}`); + } +} + +// Simple helper to simulate server-side processing latency (like Cloudflare examples) +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +// Profile Service - Cap'n Web RPC Target (following Cloudflare pattern) +class ProfileService extends RpcTarget { + constructor() { + super(); + // Store for user sessions + this.userSessions = new Map(); + } + + // Get user profile data (main RPC method) + async getProfile(accessToken) { + await sleep(Number(process.env.DELAY_PROFILE_MS ?? 80)); + console.log('ProfileService.getProfile() called with token:', accessToken ? 'present' : 'missing'); + console.log('Token type:', typeof accessToken); + console.log('Token value (first 50 chars):', typeof accessToken === 'string' ? accessToken.substring(0, 50) + '...' : accessToken); + + try { + const decoded = await verifyToken(accessToken); + const userId = decoded.sub; + console.log(`Token verified for user: ${userId}`); + + const profile = userProfiles.get(userId) || { bio: '' }; + + return { + id: userId, + email: decoded.email || decoded.name || 'Unknown User', + bio: profile.bio + }; + } catch (error) { + console.error('Token verification failed:', error); + throw new Error('Invalid access token'); + } + } + + // Update user profile data (main RPC method) + async updateProfile(accessToken, bio) { + await sleep(Number(process.env.DELAY_PROFILE_MS ?? 80)); + console.log('ProfileService.updateProfile() called'); + + try { + const decoded = await verifyToken(accessToken); + const userId = decoded.sub; + console.log(`Updating profile for user: ${userId}, bio length: ${bio?.length || 0}`); + + userProfiles.set(userId, { bio }); + + return { + success: true, + message: 'Profile updated successfully' + }; + } catch (error) { + console.error('Token verification failed:', error); + throw new Error('Invalid access token'); + } + } +} + +// Create HTTP server with WebSocket support +const server = http.createServer(async (req, res) => { + // Handle CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Parse URL to handle query parameters + const url = new URL(req.url, `http://${req.headers.host}`); + + // Serve static files for the client (handle root path with any query params) + if (url.pathname === '/' || url.pathname === '/index.html') { + try { + const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8'); + res.setHeader('Content-Type', 'text/html'); + res.writeHead(200); + res.end(html); + } catch (error) { + res.writeHead(404); + res.end('File not found'); + } + return; + } + + // Serve client JavaScript files + if (url.pathname === '/client.js') { + try { + const js = readFileSync(join(__dirname, '../client/client.js'), 'utf8'); + res.setHeader('Content-Type', 'application/javascript'); + res.writeHead(200); + res.end(js); + } catch (error) { + res.writeHead(404); + res.end('File not found'); + } + return; + } + + // Serve Cap'n Web browser implementation + if (url.pathname === '/capnweb-browser.js') { + try { + const js = readFileSync(join(__dirname, '../client/capnweb-browser.js'), 'utf8'); + res.setHeader('Content-Type', 'application/javascript'); + res.writeHead(200); + res.end(js); + } catch (error) { + res.writeHead(404); + res.end('File not found'); + } + return; + } + + // Serve Auth0 configuration (public data only) + if (url.pathname === '/api/config') { + try { + const config = { + auth0: { + domain: AUTH0_DOMAIN, + clientId: process.env.AUTH0_CLIENT_ID, + audience: AUTH0_AUDIENCE + } + }; + + // Validate that we have the required configuration + if (!config.auth0.domain || !config.auth0.clientId || !config.auth0.audience) { + res.writeHead(500); + res.end(JSON.stringify({ + error: 'Server configuration incomplete. Please check environment variables.' + })); + return; + } + + res.setHeader('Content-Type', 'application/json'); + res.writeHead(200); + res.end(JSON.stringify(config)); + } catch (error) { + res.writeHead(500); + res.end(JSON.stringify({ error: 'Configuration error' })); + } + return; + } + + res.writeHead(404); + res.end('Not found'); +}); + +// Create WebSocket server for Cap'n Web RPC +const wss = new WebSocketServer({ + server, + verifyClient: (info) => { + console.log('WebSocket connection request from:', info.origin); + return true; // Allow all connections for development + } +}); + +wss.on('connection', (ws, req) => { + console.log('New WebSocket connection established'); + + const profileService = new ProfileService(); + + ws.on('message', async (data) => { + try { + const message = JSON.parse(data.toString()); + console.log('Received RPC message:', { type: message.type, method: message.method, id: message.id }); + + if (message.type === 'call') { + try { + const result = await profileService[message.method](...(message.params || [])); + console.log(`RPC call ${message.method} succeeded`); + + ws.send(JSON.stringify({ + type: 'response', + id: message.id, + result: result + })); + } catch (error) { + console.error(`RPC call ${message.method} failed:`, error); + + ws.send(JSON.stringify({ + type: 'response', + id: message.id, + error: error.message + })); + } + } + } catch (error) { + console.error('Error handling WebSocket message:', error); + + // Send error response if we can parse the message ID + try { + const message = JSON.parse(data.toString()); + if (message.id) { + ws.send(JSON.stringify({ + type: 'response', + id: message.id, + error: 'Invalid message format' + })); + } + } catch (parseError) { + // Ignore parsing errors for error responses + } + } + }); + + ws.on('close', () => { + console.log('WebSocket connection closed'); + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); +}); + +const PORT = process.env.PORT || 3000; +const HOST = process.env.HOST || 'localhost'; + +server.listen(PORT, () => { + console.log(` +๐Ÿš€ Cap'n Web + Auth0 Demo Server +โ–ถ๏ธ Server running on http://${HOST}:${PORT} +๐Ÿ”— WebSocket RPC available for real-time communication +๐Ÿ“– API Documentation: See README.md for full setup guide + +๐Ÿ”ง Configuration: + - Auth0 Domain: ${AUTH0_DOMAIN} + - Auth0 Audience: ${AUTH0_AUDIENCE} + - Environment: ${process.env.NODE_ENV || 'development'} + +๐Ÿ‘ฅ For Developers: + 1. Copy .env.example to .env and update with your Auth0 configuration + 2. Visit http://${HOST}:${PORT} to test the application + 3. Try the pipelining demo to see concurrent RPC calls + 4. Configuration available at: http://${HOST}:${PORT}/api/config + +๐Ÿ“š Learn More: + - Auth0 Docs: https://auth0.com/docs + - Cap'n Web: https://blog.cloudflare.com/capnweb-javascript-rpc-library/ + `); +}); \ No newline at end of file diff --git a/README.md b/README.md index ed9be08..3eb7c65 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ List of available quickstarts - [01 - Login](/01-Login/) - [02 - Calling an API](/02-Calling-an-API/) +- [03 - Cap'n Web](/03-Capn-Web/) ## What is Auth0?