/** * Memento MCP Server - 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 } */ import { createHash, randomBytes } from 'crypto'; const KEY_PREFIX = 'mcp_key_'; /** * Generate a new API key. * @param {import('@prisma/client').PrismaClient} prisma * @param {object} opts * @param {string} opts.name - Human-readable name for this key * @param {string} opts.userId - User ID to link this key to * @returns {{ rawKey: string, info: object }} The raw key (show once!) and key info */ export async function generateApiKey(prisma, { name, userId }) { // Validate user exists const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, name: true, email: true, role: true }, }); if (!user) throw new Error(`User not found: ${userId}`); // Generate raw key: mcp_sk_{32 random hex chars} const rawBytes = randomBytes(24); const shortId = rawBytes.toString('hex').substring(0, 8); const rawKey = `mcp_sk_${rawBytes.toString('hex')}`; // Hash for storage const keyHash = hashKey(rawKey); const keyInfo = { shortId, name: name || `Key for ${user.name}`, userId: user.id, userName: user.name, userEmail: user.email, keyHash, createdAt: new Date().toISOString(), lastUsedAt: null, active: true, }; await prisma.systemConfig.create({ data: { key: `${KEY_PREFIX}${shortId}`, value: JSON.stringify(keyInfo), }, }); return { rawKey, // Only returned once! info: { shortId, name: keyInfo.name, userId: keyInfo.userId, userName: keyInfo.userName, createdAt: keyInfo.createdAt, }, }; } /** * Validate an API key and return the associated user info. * @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 */ export async function validateApiKey(prisma, rawKey) { if (!rawKey || !rawKey.startsWith('mcp_sk_')) return null; const keyHash = hashKey(rawKey); // Find matching key const allKeys = await prisma.systemConfig.findMany({ where: { key: { startsWith: KEY_PREFIX } }, }); for (const entry of allKeys) { try { const info = JSON.parse(entry.value); if (info.keyHash === keyHash && info.active) { // Update lastUsedAt info.lastUsedAt = new Date().toISOString(); await prisma.systemConfig.update({ where: { key: entry.key }, data: { value: JSON.stringify(info) }, }); // Return user context return { apiKeyId: info.shortId, apiKeyName: info.name, userId: info.userId, userName: info.userName, }; } } catch { // Invalid JSON, skip } } return null; } /** * List all API keys (without revealing hashes). * @param {import('@prisma/client').PrismaClient} prisma * @param {object} [opts] * @param {string} [opts.userId] - Filter by user * @returns {array} */ export async function listApiKeys(prisma, { userId } = {}) { const allKeys = await prisma.systemConfig.findMany({ where: { key: { startsWith: KEY_PREFIX } }, }); const keys = []; for (const entry of allKeys) { try { const info = JSON.parse(entry.value); if (userId && info.userId !== userId) continue; keys.push({ shortId: info.shortId, name: info.name, userId: info.userId, userName: info.userName, active: info.active, createdAt: info.createdAt, lastUsedAt: info.lastUsedAt, }); } catch { // skip } } return keys; } /** * Revoke an API key by shortId. * @param {import('@prisma/client').PrismaClient} prisma * @param {string} shortId - The key's short identifier * @returns {boolean} Whether the key was found and deactivated */ export async function revokeApiKey(prisma, shortId) { const configKey = `${KEY_PREFIX}${shortId}`; const entry = await prisma.systemConfig.findUnique({ where: { key: configKey } }); if (!entry) return false; const info = JSON.parse(entry.value); if (!info.active) return false; info.active = false; info.revokedAt = new Date().toISOString(); await prisma.systemConfig.update({ where: { key: configKey }, data: { value: JSON.stringify(info) }, }); return true; } /** * Delete an API key permanently from DB. * @param {import('@prisma/client').PrismaClient} prisma * @param {string} shortId */ export async function deleteApiKey(prisma, shortId) { const configKey = `${KEY_PREFIX}${shortId}`; try { await prisma.systemConfig.delete({ where: { key: configKey } }); return true; } catch { return false; } } /** * Resolve a user by email or ID for auth purposes. * @param {import('@prisma/client').PrismaClient} prisma * @param {string} identifier - Email or user ID * @returns {object|null} */ export async function resolveUser(prisma, identifier) { if (!identifier) return null; // Try by ID first let user = await prisma.user.findUnique({ where: { id: identifier }, select: { id: true, name: true, email: true, role: true }, }); // Try by email if (!user) { user = await prisma.user.findUnique({ where: { email: identifier }, select: { id: true, name: true, email: true, role: true }, }); } return user; } // ── Internal ────────────────────────────────────────────────────────────────── function hashKey(rawKey) { return createHash('sha256').update(rawKey).digest('hex'); }