UNPKG

@jeanmemory/react

Version:

React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code

131 lines (130 loc) 6.46 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SignInWithJean = SignInWithJean; const jsx_runtime_1 = require("react/jsx-runtime"); /** * Jean Memory React SDK - Sign In With Jean Component * OAuth 2.1 PKCE authentication flow */ const react_1 = require("react"); const config_1 = require("./config"); // Generate code verifier and challenge for PKCE function generatePKCE() { const verifier = generateRandomString(128); const encoder = new TextEncoder(); const data = encoder.encode(verifier); return crypto.subtle.digest('SHA-256', data).then(digest => { const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); return { verifier, challenge }; }); } function generateRandomString(length) { const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const values = crypto.getRandomValues(new Uint8Array(length)); return Array.from(values) .map(x => charset[x % charset.length]) .join(''); } function SignInWithJean({ onSuccess, onError, apiKey, className = '', children }) { const [isLoading, setIsLoading] = (0, react_1.useState)(false); (0, react_1.useEffect)(() => { // Handle OAuth callback const params = new URLSearchParams(window.location.search); const code = params.get('code'); const state = params.get('state'); if (code && state) { handleOAuthCallback(code, state); } }, []); const handleOAuthCallback = async (code, state) => { try { // Retrieve PKCE verifier from sessionStorage const storedState = sessionStorage.getItem('jean_oauth_state'); const verifier = sessionStorage.getItem('jean_oauth_verifier'); if (state !== storedState) { throw new Error('State mismatch - possible CSRF attack'); } if (!verifier) { throw new Error('Missing PKCE verifier'); } // Exchange code for token const response = await fetch(`${config_1.JEAN_API_BASE}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'authorization_code', code, redirect_uri: window.location.origin + window.location.pathname, code_verifier: verifier, client_id: apiKey || 'default_client' }) }); if (!response.ok) { throw new Error('Failed to exchange code for token'); } const data = await response.json(); // Get user info const userResponse = await fetch(`${config_1.JEAN_API_BASE}/api/v1/user/me`, { headers: { 'Authorization': `Bearer ${data.access_token}` } }); if (!userResponse.ok) { throw new Error('Failed to get user info'); } const user = await userResponse.json(); user.access_token = data.access_token; // Clean up sessionStorage.removeItem('jean_oauth_state'); sessionStorage.removeItem('jean_oauth_verifier'); // Remove OAuth params from URL const url = new URL(window.location.href); url.searchParams.delete('code'); url.searchParams.delete('state'); window.history.replaceState({}, '', url.toString()); onSuccess(user); } catch (error) { console.error('OAuth callback error:', error); if (onError) { onError(error instanceof Error ? error : new Error('OAuth callback failed')); } } }; const handleSignIn = async () => { setIsLoading(true); try { // Generate PKCE parameters const { verifier, challenge } = await generatePKCE(); const state = generateRandomString(32); // Store for callback sessionStorage.setItem('jean_oauth_state', state); sessionStorage.setItem('jean_oauth_verifier', verifier); // Build OAuth URL const params = new URLSearchParams({ response_type: 'code', client_id: apiKey || 'default_client', redirect_uri: window.location.origin + window.location.pathname, state, code_challenge: challenge, code_challenge_method: 'S256', scope: 'read write' }); // Redirect to OAuth provider window.location.href = `${config_1.JEAN_OAUTH_BASE}/oauth/authorize?${params.toString()}`; } catch (error) { setIsLoading(false); console.error('Sign in error:', error); if (onError) { onError(error instanceof Error ? error : new Error('Sign in failed')); } } }; return ((0, jsx_runtime_1.jsx)("button", { onClick: handleSignIn, disabled: isLoading, className: `inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-gray-900 dark:bg-white text-white dark:text-gray-900 font-medium text-sm rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-900 dark:focus:ring-gray-300 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${className}`, children: isLoading ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: "w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" }), (0, jsx_runtime_1.jsx)("span", { children: "Signing in..." })] })) : children || ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 2L13.09 8.26L20 9L13.09 15.74L12 22L10.91 15.74L4 9L10.91 8.26L12 2Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }), (0, jsx_runtime_1.jsx)("span", { children: "Sign In with Jean" })] })) })); }