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
JavaScript
"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