- Add reminders page with navigation support - Upgrade BMad builder module to skills-based architecture - Refactor MCP server: extract tools and auth into separate modules - Add connections cache, custom AI provider support - Update prisma schema and generated client - Various UI/UX improvements and i18n updates - Add service worker for PWA support Made-with: Cursor
220 lines
5.8 KiB
JavaScript
220 lines
5.8 KiB
JavaScript
/**
|
|
* 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');
|
|
}
|