Enable static site deployment with OAuth callback support

- 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 <noreply@anthropic.com>
This commit is contained in:
Jerome 2025-06-18 20:03:20 +01:00
commit 8ae2da590f
No known key found for this signature in database
4 changed files with 31 additions and 15 deletions

View file

@ -4,17 +4,24 @@ import { ConversationApp } from '@/components/ConversationApp'
import { OAuthCallback } from '@/components/OAuthCallback' import { OAuthCallback } from '@/components/OAuthCallback'
function App() { function App() {
// Simple routing based on pathname // Check if this is an OAuth callback based on query parameters
const pathname = window.location.pathname; const urlParams = new URLSearchParams(window.location.search);
const isInferenceOAuthCallback = pathname === '/oauth/inference/callback'; const code = urlParams.get('code');
const isMcpOAuthCallback = pathname === '/oauth/mcp/callback'; const state = urlParams.get('state');
if (isInferenceOAuthCallback) { // Determine OAuth callback type from state parameter
return <OAuthCallback type="inference" />; 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) { if (oauthType) {
return <OAuthCallback type="mcp" />; return <OAuthCallback type={oauthType} />;
} }
return ( return (

View file

@ -69,7 +69,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
} }
get redirectUrl(): string { get redirectUrl(): string {
return `${window.location.origin}/oauth/mcp/callback`; return `${window.location.origin}${window.location.pathname}`;
} }
get clientMetadata(): OAuthClientMetadata { get clientMetadata(): OAuthClientMetadata {
@ -85,7 +85,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
state(): string { state(): string {
// Encode connection ID in the state parameter for callback identification // Encode connection ID in the state parameter for callback identification
const randomPart = this.generateRandomString(8); const randomPart = this.generateRandomString(8);
return `${this.connectionId}.${randomPart}`; return `mcp:${this.connectionId}.${randomPart}`;
} }
// Generate a consistent key for the server based on URL // Generate a consistent key for the server based on URL
@ -141,7 +141,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
// Extract connection ID from state parameter // Extract connection ID from state parameter
const state = event.data.state; const state = event.data.state;
if (!state || !state.startsWith(this.connectionId + '.')) return; if (!state || !state.startsWith(`mcp:${this.connectionId}.`)) return;
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
popup.close(); popup.close();

View file

@ -33,7 +33,7 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
constructor(config?: OpenRouterOAuthConfig) { constructor(config?: OpenRouterOAuthConfig) {
super(); super();
this.oauthConfig = { this.oauthConfig = {
redirectUri: `${window.location.origin}/oauth/inference/callback`, redirectUri: `${window.location.origin}${window.location.pathname}`,
...config, ...config,
}; };
this.client = new OpenRouterClient(config || {}); this.client = new OpenRouterClient(config || {});
@ -253,7 +253,11 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
} }
const storedState: OAuthState = JSON.parse(storedStateJson); 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'); localStorage.removeItem('openrouter_oauth_state');
throw new Error('Invalid or expired OAuth state'); throw new Error('Invalid or expired OAuth state');
} }
@ -319,7 +323,9 @@ export class OpenRouterOAuthProvider extends InferenceProvider {
private generateState(): string { private generateState(): string {
const array = new Uint8Array(16); const array = new Uint8Array(16);
crypto.getRandomValues(array); 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 { private storeTokens(): void {

View file

@ -5,6 +5,9 @@ import path from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], 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: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),