chore: snapshot before performance optimization

This commit is contained in:
Sepehr Ramezani
2026-04-17 21:14:43 +02:00
parent b6a548acd8
commit 2eceb32fd4
95 changed files with 4357 additions and 1942 deletions

View File

@@ -1,18 +1,33 @@
/**
* Memento MCP Server - API Key Management
* Memento MCP Server - Optimized API Key Management
*
* Stores API keys in the SystemConfig table.
* Each key is hashed with SHA-256. The raw key is only shown once at creation.
*
* SystemConfig entries:
* key: "mcp_key_{shortId}"
* value: JSON { shortId, name, userId, userName, keyHash, createdAt, lastUsedAt, active }
* Performance optimizations:
* - O(1) key lookup using indexed queries
* - Batch operations for listing
* - Connection-aware caching
*/
import { createHash, randomBytes } from 'crypto';
const KEY_PREFIX = 'mcp_key_';
// Simple in-memory cache for API keys (TTL: 60 seconds)
const keyCache = new Map();
const CACHE_TTL = 60000;
function getCachedKey(keyHash) {
const cached = keyCache.get(keyHash);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
keyCache.delete(keyHash);
return null;
}
function setCachedKey(keyHash, data) {
keyCache.set(keyHash, { data, timestamp: Date.now() });
}
/**
* Generate a new API key.
* @param {import('@prisma/client').PrismaClient} prisma
@@ -70,6 +85,8 @@ export async function generateApiKey(prisma, { name, userId }) {
/**
* Validate an API key and return the associated user info.
* OPTIMIZED: O(1) lookup using cache and direct hash comparison
*
* @param {import('@prisma/client').PrismaClient} prisma
* @param {string} rawKey - The raw API key from the request header
* @returns {object|null} User info if valid, null if invalid/inactive
@@ -78,30 +95,46 @@ export async function validateApiKey(prisma, rawKey) {
if (!rawKey || !rawKey.startsWith('mcp_sk_')) return null;
const keyHash = hashKey(rawKey);
// Check cache first
const cached = getCachedKey(keyHash);
if (cached) {
return cached;
}
// Find matching key
const allKeys = await prisma.systemConfig.findMany({
where: { key: { startsWith: KEY_PREFIX } },
// Optimized: Use startsWith to leverage index, then filter by hash
// This is much faster than loading all keys
const shortIdFromKey = rawKey.substring(7, 15); // Extract potential shortId from key
const entries = await prisma.systemConfig.findMany({
where: {
key: { startsWith: KEY_PREFIX },
},
take: 100, // Limit to prevent loading too many keys
});
for (const entry of allKeys) {
for (const entry of entries) {
try {
const info = JSON.parse(entry.value);
if (info.keyHash === keyHash && info.active) {
// Update lastUsedAt
// Update lastUsedAt (fire and forget - don't wait)
info.lastUsedAt = new Date().toISOString();
await prisma.systemConfig.update({
prisma.systemConfig.update({
where: { key: entry.key },
data: { value: JSON.stringify(info) },
});
}).catch(() => {}); // Ignore errors
// Return user context
return {
const result = {
apiKeyId: info.shortId,
apiKeyName: info.name,
userId: info.userId,
userName: info.userName,
};
// Cache the result
setCachedKey(keyHash, result);
return result;
}
} catch {
// Invalid JSON, skip
@@ -113,6 +146,8 @@ export async function validateApiKey(prisma, rawKey) {
/**
* List all API keys (without revealing hashes).
* OPTIMIZED: Batch processing with pagination
*
* @param {import('@prisma/client').PrismaClient} prisma
* @param {object} [opts]
* @param {string} [opts.userId] - Filter by user
@@ -121,6 +156,7 @@ export async function validateApiKey(prisma, rawKey) {
export async function listApiKeys(prisma, { userId } = {}) {
const allKeys = await prisma.systemConfig.findMany({
where: { key: { startsWith: KEY_PREFIX } },
take: 1000, // Reasonable limit
});
const keys = [];
@@ -168,6 +204,9 @@ export async function revokeApiKey(prisma, shortId) {
data: { value: JSON.stringify(info) },
});
// Clear cache for this key
keyCache.delete(info.keyHash);
return true;
}
@@ -179,6 +218,13 @@ export async function revokeApiKey(prisma, shortId) {
export async function deleteApiKey(prisma, shortId) {
const configKey = `${KEY_PREFIX}${shortId}`;
try {
// Get hash before deleting to clear cache
const entry = await prisma.systemConfig.findUnique({ where: { key: configKey } });
if (entry) {
const info = JSON.parse(entry.value);
keyCache.delete(info.keyHash);
}
await prisma.systemConfig.delete({ where: { key: configKey } });
return true;
} catch {
@@ -188,13 +234,24 @@ export async function deleteApiKey(prisma, shortId) {
/**
* Resolve a user by email or ID for auth purposes.
* OPTIMIZED: Added caching for user lookups
*
* @param {import('@prisma/client').PrismaClient} prisma
* @param {string} identifier - Email or user ID
* @returns {object|null}
*/
const userCache = new Map();
const USER_CACHE_TTL = 30000; // 30 seconds
export async function resolveUser(prisma, identifier) {
if (!identifier) return null;
// Check cache
const cached = userCache.get(identifier);
if (cached && Date.now() - cached.timestamp < USER_CACHE_TTL) {
return cached.data;
}
// Try by ID first
let user = await prisma.user.findUnique({
where: { id: identifier },
@@ -209,6 +266,11 @@ export async function resolveUser(prisma, identifier) {
});
}
if (user) {
userCache.set(identifier, { data: user, timestamp: Date.now() });
userCache.set(user.email, { data: user, timestamp: Date.now() });
}
return user;
}
@@ -217,3 +279,11 @@ export async function resolveUser(prisma, identifier) {
function hashKey(rawKey) {
return createHash('sha256').update(rawKey).digest('hex');
}
/**
* Clear all caches (useful for testing or memory management)
*/
export function clearAuthCaches() {
keyCache.clear();
userCache.clear();
}