UNPKG

@jeanmemory/node

Version:

Node.js SDK for Jean Memory - Power your Next.js and other Node.js backends with a perfect memory

265 lines 10.1 kB
"use strict"; /** * Jean Memory Node.js SDK Authentication * OAuth 2.1 PKCE authentication for Node.js applications */ Object.defineProperty(exports, "__esModule", { value: true }); exports.JeanMemoryAuth = void 0; const crypto_1 = require("crypto"); const http_1 = require("http"); const url_1 = require("url"); class JeanMemoryAuth { constructor(config) { this.apiKey = config.apiKey; this.oauthBase = config.oauthBase || 'https://jean-memory-api-virginia.onrender.com'; this.redirectPort = config.redirectPort || 8080; this.redirectUri = `http://localhost:${this.redirectPort}/callback`; } /** * Generate PKCE code verifier and challenge */ generatePKCEPair() { // Generate cryptographically secure random verifier const verifier = (0, crypto_1.randomBytes)(32) .toString('base64url') .slice(0, 43); // Remove padding // Create challenge from verifier using SHA256 const challenge = (0, crypto_1.createHash)('sha256') .update(verifier) .digest('base64url'); return { verifier, challenge }; } /** * Generate secure random state parameter */ generateState() { return (0, crypto_1.randomBytes)(32).toString('base64url'); } /** * Create authorization URL for OAuth flow */ createAuthorizationUrl() { const { verifier, challenge } = this.generatePKCEPair(); const state = this.generateState(); const authParams = new URLSearchParams({ response_type: 'code', client_id: this.apiKey, redirect_uri: this.redirectUri, scope: 'read write', state, code_challenge: challenge, code_challenge_method: 'S256' }); const authUrl = `${this.oauthBase}/oauth/authorize?${authParams.toString()}`; return { url: authUrl, state, verifier }; } /** * Exchange authorization code for access token */ async exchangeCodeForToken(code, verifier) { const tokenData = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: this.redirectUri, code_verifier: verifier, client_id: this.apiKey }); const tokenResponse = await fetch(`${this.oauthBase}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: tokenData.toString() }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${errorText}`); } const tokenInfo = await tokenResponse.json(); const accessToken = tokenInfo.access_token; // Get user information const userResponse = await fetch(`${this.oauthBase}/api/v1/user/me`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!userResponse.ok) { const errorText = await userResponse.text(); throw new Error(`Failed to get user info: ${errorText}`); } const userInfo = await userResponse.json(); // Return client-safe result (no user_id) return { email: userInfo.email, name: userInfo.name, created_at: userInfo.created_at, access_token: accessToken }; } /** * Start local server for OAuth callback */ createCallbackServer() { return new Promise((resolve, reject) => { let authCode = null; let authState = null; let authError = null; const server = (0, http_1.createServer)((req, res) => { if (!req.url) { res.writeHead(400); res.end('Bad request'); return; } const parsedUrl = new url_1.URL(req.url, `http://localhost:${this.redirectPort}`); if (parsedUrl.pathname === '/callback') { const code = parsedUrl.searchParams.get('code'); const state = parsedUrl.searchParams.get('state'); const error = parsedUrl.searchParams.get('error'); if (error) { authError = error; res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(` <html> <head><title>Jean Memory Authentication Error</title></head> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h1>Authentication Failed</h1> <p>Error: ${error}</p> <p>Please try again.</p> </body> </html> `); } else if (code && state) { authCode = code; authState = state; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <head><title>Jean Memory Authentication</title></head> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h1>Authentication Successful!</h1> <p>You can now close this window and return to your application.</p> <script> setTimeout(() => window.close(), 3000); </script> </body> </html> `); } else { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(` <html> <head><title>Jean Memory Authentication Error</title></head> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h1>Authentication Failed</h1> <p>Missing authorization code or state parameter.</p> </body> </html> `); } } else { res.writeHead(404); res.end('Not found'); } }); server.listen(this.redirectPort, 'localhost', () => { const getAuthCode = () => { return new Promise((resolveCode, rejectCode) => { const checkForCode = () => { if (authError) { rejectCode(new Error(`OAuth error: ${authError}`)); } else if (authCode && authState) { resolveCode({ code: authCode, state: authState }); } else { setTimeout(checkForCode, 100); } }; checkForCode(); }); }; resolve({ server, getAuthCode }); }); server.on('error', (err) => { reject(err); }); }); } /** * Perform complete OAuth 2.1 PKCE authentication flow * This method requires user interaction (opening browser) */ async authenticate(timeout = 300000) { // Start callback server const { server, getAuthCode } = await this.createCallbackServer(); try { // Generate auth URL const { url: authUrl, state: expectedState, verifier } = this.createAuthorizationUrl(); console.log('Opening browser for authentication...'); console.log(`If the browser doesn't open automatically, visit: ${authUrl}`); // In a real implementation, you might want to open the browser automatically // For now, we'll log the URL for manual opening // Wait for callback with timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Authentication timeout')), timeout); }); const { code, state } = await Promise.race([ getAuthCode(), timeoutPromise ]); // Verify state to prevent CSRF attacks if (state !== expectedState) { throw new Error('State mismatch - possible CSRF attack'); } // Exchange code for token return await this.exchangeCodeForToken(code, verifier); } finally { // Clean up server server.close(); } } /** * Validate an existing access token */ async validateToken(accessToken) { try { const response = await fetch(`${this.oauthBase}/api/v1/user/me`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); return response.ok; } catch { return false; } } /** * Get user info from access token */ async getUserInfo(accessToken) { const response = await fetch(`${this.oauthBase}/api/v1/user/me`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to get user info: ${errorText}`); } const userInfo = await response.json(); // Return client-safe result (no user_id) return { email: userInfo.email, name: userInfo.name, created_at: userInfo.created_at, access_token: accessToken }; } } exports.JeanMemoryAuth = JeanMemoryAuth; //# sourceMappingURL=auth.js.map