@jeanmemory/react
Version:
React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code
131 lines (130 loc) • 6.46 kB
JavaScript
;
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" })] })) }));
}