Skip to main content
Glama

Karakeep MCP server

by karakeep-app
auth.ts4.13 kB
import { createHash, randomBytes } from "crypto"; import * as bcrypt from "bcryptjs"; import { and, eq } from "drizzle-orm"; import { apiKeys } from "@karakeep/db/schema"; import serverConfig from "@karakeep/shared/config"; import type { Context } from "./index"; const BCRYPT_SALT_ROUNDS = 10; const API_KEY_PREFIX_V1 = "ak1"; const API_KEY_PREFIX_V2 = "ak2"; function generateApiKeySecret() { const secret = randomBytes(16).toString("hex"); return { keyId: randomBytes(10).toString("hex"), secret, secretHash: createHash("sha256").update(secret).digest("base64"), }; } export function generatePasswordSalt() { return randomBytes(32).toString("hex"); } export async function regenerateApiKey( id: string, userId: string, database: Context["db"], ) { const { keyId, secret, secretHash } = generateApiKeySecret(); const plain = `${API_KEY_PREFIX_V2}_${keyId}_${secret}`; const res = await database .update(apiKeys) .set({ keyId: keyId, keyHash: secretHash, }) .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId))); if (res.changes == 0) { throw new Error("Failed to regenerate API key"); } return plain; } export async function generateApiKey( name: string, userId: string, database: Context["db"], ) { const { keyId, secret, secretHash } = generateApiKeySecret(); const plain = `${API_KEY_PREFIX_V2}_${keyId}_${secret}`; const key = ( await database .insert(apiKeys) .values({ name: name, userId: userId, keyId, keyHash: secretHash, }) .returning() )[0]; return { id: key.id, name: key.name, createdAt: key.createdAt, key: plain, }; } function parseApiKey(plain: string) { const parts = plain.split("_"); if (parts.length != 3) { throw new Error( `Malformd API key. API keys should have 3 segments, found ${parts.length} instead.`, ); } if (parts[0] !== API_KEY_PREFIX_V1 && parts[0] !== API_KEY_PREFIX_V2) { throw new Error(`Malformd API key. Got unexpected key prefix.`); } return { version: parts[0] == API_KEY_PREFIX_V1 ? (1 as const) : (2 as const), keyId: parts[1], keySecret: parts[2], }; } export async function authenticateApiKey(key: string, database: Context["db"]) { const { version, keyId, keySecret } = parseApiKey(key); const apiKey = await database.query.apiKeys.findFirst({ where: (k, { eq }) => eq(k.keyId, keyId), with: { user: true, }, }); if (!apiKey) { throw new Error("API key not found"); } const hash = apiKey.keyHash; let validation = false; switch (version) { case 1: validation = await bcrypt.compare(keySecret, hash); break; case 2: validation = createHash("sha256").update(keySecret).digest("base64") == hash; break; default: throw new Error("Invalid API Key"); } if (!validation) { throw new Error("Invalid API Key"); } return apiKey.user; } export async function hashPassword(password: string, salt: string | null) { return await bcrypt.hash(password + (salt ?? ""), BCRYPT_SALT_ROUNDS); } export async function validatePassword( email: string, password: string, database: Context["db"], ) { if (serverConfig.auth.disablePasswordAuth) { throw new Error("Password authentication is currently disabled"); } const user = await database.query.users.findFirst({ where: (u, { eq }) => eq(u.email, email), }); if (!user) { // Run a bcrypt comparison anyways to hide the fact of whether the user exists or not (protecting against timing attacks) await bcrypt.compare( password + "b6bfd1e907eb40462e73986f6cd628c036dc079b101186d36d53b824af3c9d2e", "a-dummy-password-that-should-never-match", ); throw new Error("User not found"); } if (!user.password) { throw new Error("This user doesn't have a password defined"); } const validation = await bcrypt.compare( password + (user.salt ?? ""), user.password, ); if (!validation) { throw new Error("Wrong password"); } return user; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/karakeep-app/karakeep'

If you have feedback or need assistance with the MCP directory API, please join our Discord server