From 8ae2da590f4dae4e466d78d49c92544296b7e753 Mon Sep 17 00:00:00 2001 From: Jerome Date: Wed, 18 Jun 2025 20:03:20 +0100 Subject: [PATCH] Enable static site deployment with OAuth callback support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update OAuth callbacks to use query parameters instead of path routing - Change redirect URIs to use current page URL (works with any base path) - Add callback type prefix to state parameter (inference: or mcp:) - Update App.tsx to route OAuth callbacks based on query params - Add base path configuration to vite.config.ts for GitHub Pages - Now works with static hosting (GitHub Pages, python http.server, etc) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/App.tsx | 25 ++++++++++++++-------- src/mcp/connection.ts | 6 +++--- src/providers/openrouter/oauth-provider.ts | 12 ++++++++--- vite.config.ts | 3 +++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 48bc69e..1ce9a7d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,17 +4,24 @@ import { ConversationApp } from '@/components/ConversationApp' import { OAuthCallback } from '@/components/OAuthCallback' function App() { - // Simple routing based on pathname - const pathname = window.location.pathname; - const isInferenceOAuthCallback = pathname === '/oauth/inference/callback'; - const isMcpOAuthCallback = pathname === '/oauth/mcp/callback'; - - if (isInferenceOAuthCallback) { - return ; + // Check if this is an OAuth callback based on query parameters + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const state = urlParams.get('state'); + + // Determine OAuth callback type from state parameter + let oauthType: 'inference' | 'mcp' | null = null; + if (code && state) { + // The state parameter includes the callback type + if (state.includes('inference:')) { + oauthType = 'inference'; + } else if (state.includes('mcp:')) { + oauthType = 'mcp'; + } } - if (isMcpOAuthCallback) { - return ; + if (oauthType) { + return ; } return ( diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts index 22ec803..e00cadd 100644 --- a/src/mcp/connection.ts +++ b/src/mcp/connection.ts @@ -69,7 +69,7 @@ class MCPOAuthProvider implements OAuthClientProvider { } get redirectUrl(): string { - return `${window.location.origin}/oauth/mcp/callback`; + return `${window.location.origin}${window.location.pathname}`; } get clientMetadata(): OAuthClientMetadata { @@ -85,7 +85,7 @@ class MCPOAuthProvider implements OAuthClientProvider { state(): string { // Encode connection ID in the state parameter for callback identification const randomPart = this.generateRandomString(8); - return `${this.connectionId}.${randomPart}`; + return `mcp:${this.connectionId}.${randomPart}`; } // Generate a consistent key for the server based on URL @@ -141,7 +141,7 @@ class MCPOAuthProvider implements OAuthClientProvider { // Extract connection ID from state parameter const state = event.data.state; - if (!state || !state.startsWith(this.connectionId + '.')) return; + if (!state || !state.startsWith(`mcp:${this.connectionId}.`)) return; window.removeEventListener('message', handleMessage); popup.close(); diff --git a/src/providers/openrouter/oauth-provider.ts b/src/providers/openrouter/oauth-provider.ts index abbfb3f..15f8623 100644 --- a/src/providers/openrouter/oauth-provider.ts +++ b/src/providers/openrouter/oauth-provider.ts @@ -33,7 +33,7 @@ export class OpenRouterOAuthProvider extends InferenceProvider { constructor(config?: OpenRouterOAuthConfig) { super(); this.oauthConfig = { - redirectUri: `${window.location.origin}/oauth/inference/callback`, + redirectUri: `${window.location.origin}${window.location.pathname}`, ...config, }; this.client = new OpenRouterClient(config || {}); @@ -253,7 +253,11 @@ export class OpenRouterOAuthProvider extends InferenceProvider { } const storedState: OAuthState = JSON.parse(storedStateJson); - if (storedState.state !== state || Date.now() > storedState.expiresAt) { + // Extract the actual state without the prefix for comparison + const stateWithoutPrefix = state.replace(/^inference:/, ''); + const storedStateWithoutPrefix = storedState.state.replace(/^inference:/, ''); + + if (storedStateWithoutPrefix !== stateWithoutPrefix || Date.now() > storedState.expiresAt) { localStorage.removeItem('openrouter_oauth_state'); throw new Error('Invalid or expired OAuth state'); } @@ -319,7 +323,9 @@ export class OpenRouterOAuthProvider extends InferenceProvider { private generateState(): string { const array = new Uint8Array(16); crypto.getRandomValues(array); - return btoa(String.fromCharCode.apply(null, Array.from(array))); + const randomPart = btoa(String.fromCharCode.apply(null, Array.from(array))); + // Include callback type in state for routing + return `inference:${randomPart}`; } private storeTokens(): void { diff --git a/vite.config.ts b/vite.config.ts index 2d42021..f4734f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,9 @@ import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + // Set base to '/' for local testing, or to your repo name for GitHub Pages + // e.g., base: '/example-remote-client/' for https://username.github.io/example-remote-client/ + base: '/', resolve: { alias: { '@': path.resolve(__dirname, './src'),