UNPKG

lanonasis-memory

Version:

Memory as a Service integration - AI-powered memory management with semantic search (Compatible with CLI v3.0.6+)

468 lines 19.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SecureApiKeyService = void 0; const vscode = __importStar(require("vscode")); const http = __importStar(require("http")); const crypto = __importStar(require("crypto")); const url_1 = require("url"); class SecureApiKeyService { constructor(context, outputChannel) { this.migrationCompleted = false; this.context = context; this.outputChannel = outputChannel; } /** * Initialize and migrate from legacy configuration if needed */ async initialize() { await this.migrateFromConfigIfNeeded(); } /** * Get API key from secure storage, or prompt if not available */ async getApiKeyOrPrompt() { // Try to get from secure storage first const apiKey = await this.getApiKey(); if (apiKey) { return apiKey; } // Check OAuth token const credential = await this.getStoredCredentials(); if (credential?.type === 'oauth') { return credential.token; } // Prompt user if not available return await this.promptForAuthentication(); } /** * Get API key from secure storage */ async getApiKey() { try { const apiKey = await this.context.secrets.get(SecureApiKeyService.API_KEY_KEY); return apiKey || null; } catch (error) { this.logError('Failed to retrieve API key from secure storage', error); return null; } } /** * Check if API key is configured */ async hasApiKey() { const apiKey = await this.getApiKey(); if (apiKey) return true; // Also check for OAuth token const authHeader = await this.getAuthenticationHeader(); return authHeader !== null; } /** * Prompt user for authentication (OAuth or API key) */ async promptForAuthentication() { const choice = await vscode.window.showQuickPick([ { label: '$(key) OAuth (Browser)', description: 'Authenticate using OAuth2 with browser (Recommended)', value: 'oauth' }, { label: '$(key) API Key', description: 'Enter API key directly', value: 'apikey' }, { label: '$(circle-slash) Cancel', description: 'Cancel authentication', value: 'cancel' } ], { placeHolder: 'Choose authentication method' }); if (!choice || choice.value === 'cancel') { return null; } if (choice.value === 'oauth') { return await this.authenticateWithOAuthFlow(); } else if (choice.value === 'apikey') { return await this.promptForApiKeyEntry(); } return null; } /** * Run the OAuth authentication flow and return the stored API key/token */ async authenticateWithOAuthFlow() { const success = await this.authenticateOAuth(); if (!success) { return null; } const apiKey = await this.getApiKey(); if (apiKey) { return apiKey; } const authHeader = await this.getAuthenticationHeader(); if (authHeader?.startsWith('Bearer ')) { return authHeader.replace('Bearer ', ''); } return null; } /** * Prompt for raw API key entry and persist it securely */ async promptForApiKeyEntry() { const apiKey = await vscode.window.showInputBox({ prompt: 'Enter your Lanonasis API Key', placeHolder: 'Get your API key from api.lanonasis.com', password: true, ignoreFocusOut: true, validateInput: (value) => { if (!value || value.trim().length === 0) { return 'API key is required'; } if (value.length < 20) { return 'API key seems too short'; } return null; } }); if (apiKey) { await this.storeApiKey(apiKey, 'apiKey'); await this.context.secrets.delete(SecureApiKeyService.AUTH_TOKEN_KEY); await this.context.secrets.delete(SecureApiKeyService.REFRESH_TOKEN_KEY); this.log('API key stored securely'); return apiKey; } return null; } /** * Authenticate with OAuth flow using PKCE */ async authenticateOAuth() { return new Promise((resolve, reject) => { // Store timeout reference to clear it on success/error let timeoutId; try { const config = vscode.workspace.getConfiguration('lanonasis'); const authUrl = config.get('authUrl') || 'https://auth.lanonasis.com'; const clientId = 'vscode-extension'; const redirectUri = `http://localhost:${SecureApiKeyService.CALLBACK_PORT}/callback`; // Generate PKCE parameters const codeVerifier = this.generateCodeVerifier(); const codeChallenge = this.generateCodeChallenge(codeVerifier); const state = this.generateState(); // Store PKCE data temporarily this.context.secrets.store('oauth_code_verifier', codeVerifier); this.context.secrets.store('oauth_state', state); // Build authorization URL const authUrlObj = new url_1.URL('/oauth/authorize', authUrl); authUrlObj.searchParams.set('client_id', clientId); authUrlObj.searchParams.set('response_type', 'code'); authUrlObj.searchParams.set('redirect_uri', redirectUri); authUrlObj.searchParams.set('scope', 'memories:read memories:write memories:delete'); authUrlObj.searchParams.set('code_challenge', codeChallenge); authUrlObj.searchParams.set('code_challenge_method', 'S256'); authUrlObj.searchParams.set('state', state); // Start callback server const server = http.createServer(async (req, res) => { try { if (!req.url) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Missing URL'); return; } const url = new url_1.URL(req.url, `http://localhost:${SecureApiKeyService.CALLBACK_PORT}`); if (url.pathname === '/callback') { const code = url.searchParams.get('code'); const returnedState = url.searchParams.get('state'); const error = url.searchParams.get('error'); // Validate state const storedState = await this.context.secrets.get('oauth_state'); if (returnedState !== storedState) { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end('<h1>Invalid state parameter</h1>'); server.close(); if (timeoutId) clearTimeout(timeoutId); reject(new Error('Invalid state parameter')); return; } if (error) { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(`<h1>OAuth Error: ${error}</h1>`); server.close(); if (timeoutId) clearTimeout(timeoutId); reject(new Error(`OAuth error: ${error}`)); return; } if (code) { // Exchange code for token const token = await this.exchangeCodeForToken(code, codeVerifier, redirectUri, authUrl); // Store token securely await this.storeApiKey(token.access_token, 'oauth'); if (token.refresh_token) { await this.context.secrets.store(SecureApiKeyService.REFRESH_TOKEN_KEY, token.refresh_token); } // Send success response res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <head><title>Authentication Success</title></head> <body> <h1 style="color: green;">✓ Authentication Successful!</h1> <p>You can close this window and return to VS Code.</p> <script>setTimeout(() => window.close(), 2000);</script> </body> </html> `); // Cleanup await this.context.secrets.delete('oauth_code_verifier'); await this.context.secrets.delete('oauth_state'); server.close(); if (timeoutId) clearTimeout(timeoutId); resolve(true); } } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found'); } } catch (err) { res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(`<h1>Error: ${err instanceof Error ? err.message : 'Unknown error'}</h1>`); server.close(); if (timeoutId) clearTimeout(timeoutId); reject(err); } }); server.listen(SecureApiKeyService.CALLBACK_PORT, 'localhost', () => { // Open browser vscode.env.openExternal(vscode.Uri.parse(authUrlObj.toString())); }); // Timeout after 5 minutes timeoutId = setTimeout(() => { server.close(); reject(new Error('OAuth authentication timeout')); }, 5 * 60 * 1000); } catch (error) { if (timeoutId) clearTimeout(timeoutId); reject(error); } }); } /** * Get authentication header (OAuth token or API key) */ async getAuthenticationHeader() { const credential = await this.getStoredCredentials(); if (credential?.type === 'oauth') { return `Bearer ${credential.token}`; } return null; } /** * Get the active credentials (OAuth token vs API key) for downstream services */ async getStoredCredentials() { // Prefer OAuth tokens when available const authToken = await this.context.secrets.get(SecureApiKeyService.AUTH_TOKEN_KEY); if (authToken) { try { const token = JSON.parse(authToken); if (token?.access_token && this.isTokenValid(token)) { return { type: 'oauth', token: token.access_token }; } } catch (error) { this.logError('Failed to parse stored OAuth token', error); } } const apiKey = await this.getApiKey(); if (apiKey) { const storedType = await this.context.secrets.get(SecureApiKeyService.CREDENTIAL_TYPE_KEY); const inferredType = storedType === 'oauth' || storedType === 'apiKey' ? storedType : (this.looksLikeJwt(apiKey) ? 'oauth' : 'apiKey'); return { type: inferredType, token: apiKey }; } return null; } /** * Delete API key from secure storage */ async deleteApiKey() { await this.context.secrets.delete(SecureApiKeyService.API_KEY_KEY); await this.context.secrets.delete(SecureApiKeyService.AUTH_TOKEN_KEY); await this.context.secrets.delete(SecureApiKeyService.REFRESH_TOKEN_KEY); await this.context.secrets.delete(SecureApiKeyService.CREDENTIAL_TYPE_KEY); this.log('API key removed from secure storage'); } /** * Store API key securely */ async storeApiKey(apiKey, type) { await this.context.secrets.store(SecureApiKeyService.API_KEY_KEY, apiKey); await this.context.secrets.store(SecureApiKeyService.CREDENTIAL_TYPE_KEY, type); } /** * Migrate API key from configuration to secure storage */ async migrateFromConfigIfNeeded() { if (this.migrationCompleted) { return; } // Check if already in secure storage const hasSecureKey = await this.hasApiKey(); if (hasSecureKey) { this.migrationCompleted = true; return; } // Check configuration for legacy API key const config = vscode.workspace.getConfiguration('lanonasis'); const legacyKey = config.get('apiKey'); if (legacyKey) { // Migrate to secure storage await this.storeApiKey(legacyKey, 'apiKey'); this.log('Migrated API key from configuration to secure storage'); // Optionally clear from config (but keep it for now for backward compatibility) // await config.update('apiKey', undefined, vscode.ConfigurationTarget.Global); // Notify user vscode.window.showInformationMessage('API key migrated to secure storage. Your credentials are now stored securely.', 'OK'); } this.migrationCompleted = true; } /** * Exchange OAuth authorization code for token */ async exchangeCodeForToken(code, codeVerifier, redirectUri, authUrl) { const tokenUrl = new url_1.URL('/oauth/token', authUrl); const body = new url_1.URLSearchParams({ grant_type: 'authorization_code', client_id: 'vscode-extension', code, redirect_uri: redirectUri, code_verifier: codeVerifier }); const response = await fetch(tokenUrl.toString(), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: body.toString() }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Token exchange failed: ${response.status} ${errorText}`); } const tokenData = await response.json(); // Store token with expiration const token = { access_token: tokenData.access_token, expires_at: Date.now() + (tokenData.expires_in ? tokenData.expires_in * 1000 : 3600000) }; await this.context.secrets.store(SecureApiKeyService.AUTH_TOKEN_KEY, JSON.stringify(token)); return { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token }; } /** * Check if OAuth token is valid */ isTokenValid(token) { if (!token.expires_at) return true; return Date.now() < token.expires_at - 60000; // 1 minute buffer } looksLikeJwt(value) { const parts = value.split('.'); if (parts.length !== 3) { return false; } const jwtSegment = /^[A-Za-z0-9-_]+$/; return parts.every(segment => jwtSegment.test(segment)); } /** * Generate PKCE code verifier */ generateCodeVerifier() { return crypto.randomBytes(32).toString('base64url'); } /** * Generate PKCE code challenge */ generateCodeChallenge(verifier) { return crypto.createHash('sha256').update(verifier).digest('base64url'); } /** * Generate OAuth state parameter */ generateState() { return crypto.randomBytes(16).toString('hex'); } /** * Log message to output channel */ log(message) { const timestamp = new Date().toISOString(); this.outputChannel.appendLine(`[${timestamp}] [SecureApiKeyService] ${message}`); } /** * Log error to output channel */ logError(message, error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`${message}: ${errorMessage}`); console.error(message, error); } } exports.SecureApiKeyService = SecureApiKeyService; SecureApiKeyService.API_KEY_KEY = 'lanonasis.apiKey'; SecureApiKeyService.AUTH_TOKEN_KEY = 'lanonasis.authToken'; SecureApiKeyService.REFRESH_TOKEN_KEY = 'lanonasis.refreshToken'; SecureApiKeyService.CREDENTIAL_TYPE_KEY = 'lanonasis.credentialType'; SecureApiKeyService.CALLBACK_PORT = 8080; //# sourceMappingURL=SecureApiKeyService.js.map