From 81a8c15da8ff240141d15c719e89bc429eeab531 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 16:47:33 +1000 Subject: [PATCH 1/7] wip --- examples/chat-ui/src/App.tsx | 1 + examples/chat-ui/src/types/models.ts | 10 +++++----- examples/chat-ui/src/utils/auth.ts | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/chat-ui/src/App.tsx b/examples/chat-ui/src/App.tsx index f390de7..90b40c4 100644 --- a/examples/chat-ui/src/App.tsx +++ b/examples/chat-ui/src/App.tsx @@ -6,6 +6,7 @@ function App() { return ( + } /> } /> } /> } /> diff --git a/examples/chat-ui/src/types/models.ts b/examples/chat-ui/src/types/models.ts index 73c4d16..bfcbd10 100644 --- a/examples/chat-ui/src/types/models.ts +++ b/examples/chat-ui/src/types/models.ts @@ -67,12 +67,12 @@ export const providers: Record = { baseUrl: 'https://api.groq.com/openai/v1', logo: '🚀', documentationUrl: 'https://console.groq.com/docs', - authType: 'apiKey', apiKeyHeader: 'Authorization', - // oauth: { - // authorizeUrl: 'http://localhost:3000/keys/request', - // tokenUrl: 'https://openrouter.ai/api/v1/auth/keys' - // }, + authType: 'oauth', + oauth: { + authorizeUrl: 'http://localhost:3000/keys/request', + tokenUrl: 'http://localhost:3000/keys/request/exchange', + }, }, anthropic: { id: 'anthropic', diff --git a/examples/chat-ui/src/utils/auth.ts b/examples/chat-ui/src/utils/auth.ts index d476cfe..762f4e4 100644 --- a/examples/chat-ui/src/utils/auth.ts +++ b/examples/chat-ui/src/utils/auth.ts @@ -152,7 +152,7 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str // Retrieve PKCE state let pkceState: PKCEState - if (state === 'no-state' && providerId === 'openrouter') { + if (state === 'no-state' && (providerId === 'openrouter' || providerId === 'groq')) { // OpenRouter doesn't use state, find the most recent PKCE state for this provider const allKeys = Object.keys(sessionStorage) const pkceKeys = allKeys.filter((key) => key.startsWith(`pkce_${providerId}_`)) @@ -222,6 +222,7 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str const requestBody = new URLSearchParams({ grant_type: 'authorization_code', code, + // TODO: state?? redirect_uri: getRedirectUri(providerId), code_verifier: pkceState.code_verifier, }) From d612d5aecf44dac3230dc2d817a17b9274283faf Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 20:42:38 +1000 Subject: [PATCH 2/7] Successfully getting a token from Groq! --- .../chat-ui/src/components/OAuthCallback.tsx | 6 +- examples/chat-ui/src/utils/auth.ts | 142 +++++++++++------- 2 files changed, 92 insertions(+), 56 deletions(-) diff --git a/examples/chat-ui/src/components/OAuthCallback.tsx b/examples/chat-ui/src/components/OAuthCallback.tsx index 01beae1..370dd56 100644 --- a/examples/chat-ui/src/components/OAuthCallback.tsx +++ b/examples/chat-ui/src/components/OAuthCallback.tsx @@ -34,9 +34,9 @@ const OAuthCallback: React.FC = ({ provider }) => { throw new Error('Missing authorization code') } - // OpenRouter doesn't use state parameter, but other providers might - const stateToUse = state || 'no-state' - await completeOAuthFlow(provider, code, stateToUse) + // TODO: Add state parameter handling back if needed later + // const stateToUse = state || 'no-state' + await completeOAuthFlow(provider, code) setStatus('success') // Close popup after successful authentication diff --git a/examples/chat-ui/src/utils/auth.ts b/examples/chat-ui/src/utils/auth.ts index 762f4e4..0ce1204 100644 --- a/examples/chat-ui/src/utils/auth.ts +++ b/examples/chat-ui/src/utils/auth.ts @@ -11,7 +11,8 @@ export interface OAuthToken { // Types for PKCE flow interface PKCEState { code_verifier: string - state: string + // TODO: Add state support back if needed later + // state: string } // Generate a random code verifier for PKCE @@ -35,15 +36,15 @@ async function generateCodeChallenge(verifier: string): Promise { .replace(/=/g, '') } -// Generate random state parameter -function generateState(): string { - const array = new Uint8Array(16) - crypto.getRandomValues(array) - return btoa(String.fromCharCode(...array)) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, '') -} +// TODO: Add state generation back if needed later +// function generateState(): string { +// const array = new Uint8Array(16) +// crypto.getRandomValues(array) +// return btoa(String.fromCharCode(...array)) +// .replace(/\+/g, '-') +// .replace(/\//g, '_') +// .replace(/=/g, '') +// } // API Key functions (existing functionality) export function hasApiKey(providerId: SupportedProvider): boolean { @@ -111,11 +112,17 @@ export async function beginOAuthFlow(providerId: SupportedProvider): Promise { - console.log('DEBUG: Starting OAuth completion for', providerId, 'with code:', code?.substring(0, 10) + '...', 'state:', state) +export async function completeOAuthFlow(providerId: SupportedProvider, code: string): Promise { + console.log('DEBUG: Starting OAuth completion for', providerId, 'with code:', code?.substring(0, 10) + '...') + console.log('DEBUG: Full code for debugging:', code) + console.log('DEBUG: Provider config:', providers[providerId]) const provider = providers[providerId] if (!provider.oauth) { throw new Error(`Provider ${providerId} does not support OAuth`) } - // Retrieve PKCE state - let pkceState: PKCEState + // Retrieve PKCE state (find the most recent one since we don't use state parameter) + const allKeys = Object.keys(sessionStorage) + const pkceKeys = allKeys.filter((key) => key.startsWith(`pkce_${providerId}_`)) - if (state === 'no-state' && (providerId === 'openrouter' || providerId === 'groq')) { - // OpenRouter doesn't use state, find the most recent PKCE state for this provider - const allKeys = Object.keys(sessionStorage) - const pkceKeys = allKeys.filter((key) => key.startsWith(`pkce_${providerId}_`)) + console.log('DEBUG: Found PKCE keys:', pkceKeys) - console.log('DEBUG: Found PKCE keys:', pkceKeys) + if (pkceKeys.length === 0) { + throw new Error('PKCE state not found. Please try again.') + } - if (pkceKeys.length === 0) { - throw new Error('PKCE state not found. Please try again.') - } + // Use the most recent one (sort by timestamp) + const sortedKeys = pkceKeys.sort((a, b) => { + const aTime = parseInt(a.split('_').pop() || '0') + const bTime = parseInt(b.split('_').pop() || '0') + return bTime - aTime // Most recent first + }) - // Use the most recent one (sort by timestamp) - const sortedKeys = pkceKeys.sort((a, b) => { - const aTime = parseInt(a.split('_').pop() || '0') - const bTime = parseInt(b.split('_').pop() || '0') - return bTime - aTime // Most recent first - }) + const pkceStateJson = sessionStorage.getItem(sortedKeys[0])! + const pkceState: PKCEState = JSON.parse(pkceStateJson) - const pkceStateJson = sessionStorage.getItem(sortedKeys[0])! - pkceState = JSON.parse(pkceStateJson) + console.log('DEBUG: Using PKCE state:', { key: sortedKeys[0], state: pkceState }) + console.log('DEBUG: Code verifier length:', pkceState.code_verifier.length) + console.log('DEBUG: Code verifier sample:', pkceState.code_verifier.substring(0, 20) + '...') - console.log('DEBUG: Using PKCE state:', { key: sortedKeys[0], state: pkceState }) + // Test: regenerate challenge from verifier to verify it matches server expectation + const recomputedChallenge = await generateCodeChallenge(pkceState.code_verifier) + console.log('DEBUG: Recomputed challenge from verifier:', recomputedChallenge) + console.log('DEBUG: Server reported challenge was: dW2iEvNljlkhcRcryo3Z0GITcJM1liKcHlB5v8CDEu8') - // Clean up the state - sessionStorage.removeItem(sortedKeys[0]) - } else { - const pkceStateJson = sessionStorage.getItem(`pkce_${providerId}_${state}`) - if (!pkceStateJson) { - throw new Error('PKCE state not found. Please try again.') - } - pkceState = JSON.parse(pkceStateJson) - console.log('DEBUG: Using PKCE state for state', state, ':', pkceState) - } + // Clean up the state + sessionStorage.removeItem(sortedKeys[0]) // Exchange code for token let tokenResponse: Response @@ -218,19 +224,23 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str duration: `${endTime - startTime}ms`, }) } else { - // Standard OAuth2 flow for other providers + // Standard OAuth2 flow for other providers (Groq) const requestBody = new URLSearchParams({ grant_type: 'authorization_code', code, - // TODO: state?? redirect_uri: getRedirectUri(providerId), code_verifier: pkceState.code_verifier, }) - console.log('DEBUG: Standard OAuth token request:', { + + console.log('DEBUG: Groq token request:', { url: provider.oauth.tokenUrl, body: Object.fromEntries(requestBody.entries()), + codeVerifierLength: pkceState.code_verifier.length, + codeVerifierSample: pkceState.code_verifier.substring(0, 20) + '...', + fullCodeVerifier: pkceState.code_verifier, // For debugging }) + const startTime = performance.now() tokenResponse = await fetch(provider.oauth.tokenUrl, { method: 'POST', headers: { @@ -238,11 +248,13 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str }, body: requestBody, }) + const endTime = performance.now() - console.log('DEBUG: Standard OAuth token response:', { + console.log('DEBUG: Groq token response:', { status: tokenResponse.status, statusText: tokenResponse.statusText, headers: Object.fromEntries(tokenResponse.headers.entries()), + duration: `${endTime - startTime}ms`, }) } @@ -276,10 +288,7 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str setOAuthToken(providerId, token) - // Clean up PKCE state (already cleaned up above for OpenRouter) - if (state !== 'no-state') { - sessionStorage.removeItem(`pkce_${providerId}_${state}`) - } + // PKCE state already cleaned up above } // Get authentication headers for API calls @@ -319,3 +328,30 @@ function getRedirectUri(providerId: SupportedProvider): string { const baseUrl = window.location.origin return `${baseUrl}/oauth/${providerId}/callback` } + +// Test function to verify PKCE implementation with known values +export async function testPKCEImplementation(): Promise { + console.log('=== TESTING PKCE IMPLEMENTATION ===') + + // Test with a known code verifier (from RFC 7636 example) + const testVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' + const expectedChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' + + const computedChallenge = await generateCodeChallenge(testVerifier) + + console.log('DEBUG: Test verifier:', testVerifier) + console.log('DEBUG: Expected challenge:', expectedChallenge) + console.log('DEBUG: Computed challenge:', computedChallenge) + console.log('DEBUG: Challenges match:', computedChallenge === expectedChallenge) + + // Test with current implementation + const currentVerifier = generateCodeVerifier() + const currentChallenge = await generateCodeChallenge(currentVerifier) + + console.log('DEBUG: Current verifier:', currentVerifier) + console.log('DEBUG: Current challenge:', currentChallenge) + console.log('DEBUG: Verifier length:', currentVerifier.length) + console.log('DEBUG: Challenge length:', currentChallenge.length) + + console.log('=== END PKCE TEST ===') +} From 730ba7b0c5018bf2019646cdab5b07e0d30aaf94 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 20:57:13 +1000 Subject: [PATCH 3/7] Simplify PKCE flow to use single key per provider Previously, the PKCE flow stored multiple keys with timestamps (pkce_${providerId}_${timestamp}) and had complex logic to find the most recent key. This simplified implementation: - Changes storage key format from `pkce_${providerId}_${timestamp}` to `pkce_${providerId}` - Removes timestamp-based key sorting logic in completeOAuthFlow() - Ensures only one PKCE key is stored per provider at any time - Eliminates potential race conditions with multiple concurrent auth flows - Simplifies debugging by having predictable key names This applies to both OpenRouter and Groq OAuth flows. Also fixes TypeScript build error by removing unused 'state' variable in OAuthCallback component. Amp-Thread: https://ampcode.com/threads/T-12223468-a950-40a8-9d44-2e1a66e6efbe Co-authored-by: Amp --- examples/chat-ui/src/components/ChatApp.tsx | 2 + .../chat-ui/src/components/ModelSelector.tsx | 2 + .../chat-ui/src/components/OAuthCallback.tsx | 58 +++++++++++++++---- examples/chat-ui/src/utils/auth.ts | 30 +++++----- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/examples/chat-ui/src/components/ChatApp.tsx b/examples/chat-ui/src/components/ChatApp.tsx index 6db7a43..0dc1760 100644 --- a/examples/chat-ui/src/components/ChatApp.tsx +++ b/examples/chat-ui/src/components/ChatApp.tsx @@ -33,7 +33,9 @@ const ChatApp: React.FC = () => { // Handle OAuth success messages from popups useEffect(() => { const handleMessage = (event: MessageEvent) => { + console.log('DEBUG: Received message in parent window:', event.data) if (event.data.type === 'oauth_success') { + console.log('DEBUG: OAuth success message received, triggering API key update') handleApiKeyUpdate() } } diff --git a/examples/chat-ui/src/components/ModelSelector.tsx b/examples/chat-ui/src/components/ModelSelector.tsx index 973e6b1..b87592c 100644 --- a/examples/chat-ui/src/components/ModelSelector.tsx +++ b/examples/chat-ui/src/components/ModelSelector.tsx @@ -39,7 +39,9 @@ const ModelSelector: React.FC = ({ selectedModel, onModelCha // Handle OAuth success - show provider models when OAuth completes useEffect(() => { const handleMessage = (event: MessageEvent) => { + console.log('DEBUG: ModelSelector received message:', event.data) if (event.data.type === 'oauth_success' && event.data.provider) { + console.log('DEBUG: ModelSelector opening provider models modal for:', event.data.provider) const provider = providers[event.data.provider as keyof typeof providers] if (provider) { setProviderModelsModal({ isOpen: true, provider }) diff --git a/examples/chat-ui/src/components/OAuthCallback.tsx b/examples/chat-ui/src/components/OAuthCallback.tsx index 370dd56..9355bb3 100644 --- a/examples/chat-ui/src/components/OAuthCallback.tsx +++ b/examples/chat-ui/src/components/OAuthCallback.tsx @@ -23,7 +23,6 @@ const OAuthCallback: React.FC = ({ provider }) => { try { const code = searchParams.get('code') - const state = searchParams.get('state') const error = searchParams.get('error') if (error) { @@ -39,17 +38,49 @@ const OAuthCallback: React.FC = ({ provider }) => { await completeOAuthFlow(provider, code) setStatus('success') + console.log('DEBUG: OAuth flow completed successfully') + console.log('DEBUG: window.opener exists:', !!window.opener) + console.log('DEBUG: window.opener closed:', window.opener?.closed) + console.log('DEBUG: window.parent exists:', !!window.parent) + console.log('DEBUG: window.parent === window:', window.parent === window) + + // Try multiple approaches to communicate with parent + const sendSuccessMessage = () => { + const message = { type: 'oauth_success', provider } + + // Try window.opener first + if (window.opener && !window.opener.closed) { + console.log('DEBUG: Sending message via window.opener') + window.opener.postMessage(message, '*') + } + + // Also try window.parent as fallback + if (window.parent && window.parent !== window) { + console.log('DEBUG: Sending message via window.parent') + window.parent.postMessage(message, '*') + } + + // Also try top window + if (window.top && window.top !== window) { + console.log('DEBUG: Sending message via window.top') + window.top.postMessage(message, '*') + } + } + + // Send success message immediately + sendSuccessMessage() + // Close popup after successful authentication - // Give extra time for debugging in development - setTimeout(() => { - if (window.opener) { - window.opener.postMessage({ type: 'oauth_success', provider }, '*') + if (window.opener && !window.opener.closed) { + console.log('DEBUG: Closing popup in 100ms') + setTimeout(() => { + console.log('DEBUG: Attempting to close popup') window.close() - } else { - // Redirect to main page if not in popup - window.location.href = '/' - } - }, 3000) + }, 100) + } else { + console.log('DEBUG: No valid opener, showing success message and manual close') + // Don't redirect immediately, let user see success and close manually + } } catch (err) { console.error('OAuth callback error:', err) setError(err instanceof Error ? err.message : 'Unknown error') @@ -86,7 +117,12 @@ const OAuthCallback: React.FC = ({ provider }) => {

Authentication Successful!

Successfully connected to {provider}. You can now close this window.

+

Successfully connected to {provider}.

+
+ + {window.opener && ( + + )} +
)} diff --git a/examples/chat-ui/src/utils/auth.ts b/examples/chat-ui/src/utils/auth.ts index 23383d4..b1f46ff 100644 --- a/examples/chat-ui/src/utils/auth.ts +++ b/examples/chat-ui/src/utils/auth.ts @@ -144,14 +144,18 @@ export async function beginOAuthFlow(providerId: SupportedProvider): Promise { @@ -272,6 +276,12 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str access_token: tokenData.key, token_type: 'Bearer', } + } else if (providerId === 'groq') { + // Groq returns { api_key: "..." } + token = { + access_token: tokenData.api_key, + token_type: 'Bearer', + } } else { // Standard OAuth2 response token = { @@ -282,6 +292,7 @@ export async function completeOAuthFlow(providerId: SupportedProvider, code: str } } + console.log('DEBUG: Saving token for', providerId, ':', token) setOAuthToken(providerId, token) // PKCE state already cleaned up above From aae9e985343101dea152dc6eee764472d6758acb Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 21:25:27 +1000 Subject: [PATCH 5/7] Able to run inference against staging Groq --- .../chat-ui/src/components/OAuthCallback.tsx | 36 +++++-------------- .../chat-ui/src/hooks/useStreamResponse.ts | 2 +- examples/chat-ui/src/types/models.ts | 11 +++++- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/examples/chat-ui/src/components/OAuthCallback.tsx b/examples/chat-ui/src/components/OAuthCallback.tsx index 4fcc6e1..8e0e6eb 100644 --- a/examples/chat-ui/src/components/OAuthCallback.tsx +++ b/examples/chat-ui/src/components/OAuthCallback.tsx @@ -67,19 +67,13 @@ const OAuthCallback: React.FC = ({ provider }) => { // Try to send success message const messageSent = sendSuccessMessage() + console.log({ messageSent }) - // Close popup if we have a valid opener, otherwise redirect to main page - if (window.opener && !window.opener.closed) { - console.log('DEBUG: Closing popup in 100ms') - setTimeout(() => { - console.log('DEBUG: Attempting to close popup') - window.close() - }, 100) - } else { - console.log('DEBUG: No valid opener, showing success message') - // Just show success message, let user navigate back manually - // or provide a link to go back - } + console.log('DEBUG: Closing popup in 100ms') + setTimeout(() => { + console.log('DEBUG: Attempting to close popup') + window.close() + }, 100) } catch (err) { console.error('OAuth callback error:', err) setError(err instanceof Error ? err.message : 'Unknown error') @@ -118,25 +112,13 @@ const OAuthCallback: React.FC = ({ provider }) => {
- {window.opener && ( - - )}
)} diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index dbfdec0..f8e9b6b 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -90,7 +90,7 @@ export const useStreamResponse = ({ switch (model.provider.id) { case 'groq': { const apiKey = authHeaders.Authorization?.replace('Bearer ', '') - const groqProvider = createGroq({ apiKey }) + const groqProvider = createGroq({ apiKey, baseURL: model.provider.baseUrl }) baseModel = groqProvider(model.modelId) break } diff --git a/examples/chat-ui/src/types/models.ts b/examples/chat-ui/src/types/models.ts index bfcbd10..937476d 100644 --- a/examples/chat-ui/src/types/models.ts +++ b/examples/chat-ui/src/types/models.ts @@ -61,10 +61,19 @@ export interface Model { } export const providers: Record = { + // groq: { + // id: 'groq', + // name: 'Groq', + // baseUrl: 'https://api.groq.com/openai/v1', + // logo: '🚀', + // documentationUrl: 'https://console.groq.com/docs', + // apiKeyHeader: 'Authorization', + // authType: 'apiKey', + // }, groq: { id: 'groq', name: 'Groq', - baseUrl: 'https://api.groq.com/openai/v1', + baseUrl: 'http://localhost:8000/api/openai/v1', logo: '🚀', documentationUrl: 'https://console.groq.com/docs', apiKeyHeader: 'Authorization', From 255711ab84cc9b8a50e36a54821f15971e32f04d Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 21:27:04 +1000 Subject: [PATCH 6/7] The old oauth callback got clobbered, try to restore it --- examples/chat-ui/src/App.tsx | 8 ++++---- .../components/{OAuthCallback.tsx => PkceCallback.tsx} | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename examples/chat-ui/src/components/{OAuthCallback.tsx => PkceCallback.tsx} (98%) diff --git a/examples/chat-ui/src/App.tsx b/examples/chat-ui/src/App.tsx index 90b40c4..4c56287 100644 --- a/examples/chat-ui/src/App.tsx +++ b/examples/chat-ui/src/App.tsx @@ -1,14 +1,14 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import ChatApp from './components/ChatApp' -import OAuthCallback from './components/OAuthCallback' +import PkceCallback from './components/PkceCallback.tsx' function App() { return ( - } /> - } /> - } /> + } /> + } /> + } /> } /> diff --git a/examples/chat-ui/src/components/OAuthCallback.tsx b/examples/chat-ui/src/components/PkceCallback.tsx similarity index 98% rename from examples/chat-ui/src/components/OAuthCallback.tsx rename to examples/chat-ui/src/components/PkceCallback.tsx index 8e0e6eb..0502ef3 100644 --- a/examples/chat-ui/src/components/OAuthCallback.tsx +++ b/examples/chat-ui/src/components/PkceCallback.tsx @@ -7,7 +7,7 @@ interface OAuthCallbackProps { provider: SupportedProvider } -const OAuthCallback: React.FC = ({ provider }) => { +const PkceCallback: React.FC = ({ provider }) => { const [searchParams] = useSearchParams() const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') const [error, setError] = useState(null) @@ -150,4 +150,4 @@ const OAuthCallback: React.FC = ({ provider }) => { ) } -export default OAuthCallback +export default PkceCallback From a37724e80b2d29931e1deab77301bcea8ab07331 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Fri, 11 Jul 2025 21:34:41 +1000 Subject: [PATCH 7/7] Restored, but still buggy --- examples/chat-ui/src/App.tsx | 3 ++- .../chat-ui/src/components/OAuthCallback.tsx | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 examples/chat-ui/src/components/OAuthCallback.tsx diff --git a/examples/chat-ui/src/App.tsx b/examples/chat-ui/src/App.tsx index 4c56287..59c2878 100644 --- a/examples/chat-ui/src/App.tsx +++ b/examples/chat-ui/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import ChatApp from './components/ChatApp' import PkceCallback from './components/PkceCallback.tsx' +import { OAuthCallback } from './components/OAuthCallback.tsx' function App() { return ( @@ -8,7 +9,7 @@ function App() { } /> } /> - } /> + } /> } />
diff --git a/examples/chat-ui/src/components/OAuthCallback.tsx b/examples/chat-ui/src/components/OAuthCallback.tsx new file mode 100644 index 0000000..d6f57fa --- /dev/null +++ b/examples/chat-ui/src/components/OAuthCallback.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react' +import { onMcpAuthorization } from 'use-mcp' + +export function OAuthCallback() { + useEffect(() => { + onMcpAuthorization() + }, []) + + return ( +
+
+

Authenticating...

+

Please wait while we complete your authentication.

+

This window should close automatically.

+ +
+
+
+
+
+ ) +}