@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
JavaScript
;
/**
* 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