mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
- API key generation with SHA-256 hashing and crumbs_ prefix - User-scoped key validation, listing, and deletion - Bearer token auth in hooks.server.ts for /api/mcp paths - Populates event.locals.user from API key for downstream scoping - 13 unit tests covering CRUD, validation, and user isolation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
2.5 KiB
TypeScript
106 lines
2.5 KiB
TypeScript
import { db } from './db/index.js';
|
|
import { apiKeys, users } from './db/schema.js';
|
|
import { eq } from 'drizzle-orm';
|
|
import { randomBytes, createHash } from 'crypto';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
import type * as schema from './db/schema.js';
|
|
|
|
type Db = BetterSQLite3Database<typeof schema>;
|
|
|
|
const PREFIX = 'crumbs_';
|
|
|
|
function hashKey(key: string): string {
|
|
return createHash('sha256').update(key).digest('hex');
|
|
}
|
|
|
|
export function generateApiKey(
|
|
name: string,
|
|
userId: number,
|
|
database: Db = db
|
|
): { id: string; key: string; name: string; keyPrefix: string } {
|
|
const id = uuidv4();
|
|
const raw = randomBytes(32).toString('hex');
|
|
const key = `${PREFIX}${raw}`;
|
|
const keyHash = hashKey(key);
|
|
const keyPrefix = key.slice(0, 12);
|
|
|
|
database
|
|
.insert(apiKeys)
|
|
.values({
|
|
id,
|
|
userId,
|
|
name,
|
|
keyHash,
|
|
keyPrefix,
|
|
createdAt: new Date()
|
|
})
|
|
.run();
|
|
|
|
return { id, key, name, keyPrefix };
|
|
}
|
|
|
|
export function validateApiKey(
|
|
key: string,
|
|
database: Db = db
|
|
): { valid: boolean; userId: number | null } {
|
|
if (!key || !key.startsWith(PREFIX)) return { valid: false, userId: null };
|
|
|
|
const keyHash = hashKey(key);
|
|
const row = database.select().from(apiKeys).where(eq(apiKeys.keyHash, keyHash)).get();
|
|
if (!row) return { valid: false, userId: null };
|
|
|
|
database
|
|
.update(apiKeys)
|
|
.set({ lastUsedAt: new Date() })
|
|
.where(eq(apiKeys.id, row.id))
|
|
.run();
|
|
|
|
return { valid: true, userId: row.userId };
|
|
}
|
|
|
|
export function getUserForApiKey(
|
|
userId: number,
|
|
database: Db = db
|
|
): App.Locals['user'] | null {
|
|
const user = database
|
|
.select({
|
|
id: users.id,
|
|
email: users.email,
|
|
displayName: users.displayName,
|
|
role: users.role
|
|
})
|
|
.from(users)
|
|
.where(eq(users.id, userId))
|
|
.get();
|
|
|
|
return user ? (user as App.Locals['user']) : null;
|
|
}
|
|
|
|
export function listApiKeys(userId: number, database: Db = db) {
|
|
return database
|
|
.select({
|
|
id: apiKeys.id,
|
|
name: apiKeys.name,
|
|
keyPrefix: apiKeys.keyPrefix,
|
|
createdAt: apiKeys.createdAt,
|
|
lastUsedAt: apiKeys.lastUsedAt
|
|
})
|
|
.from(apiKeys)
|
|
.where(eq(apiKeys.userId, userId))
|
|
.all();
|
|
}
|
|
|
|
export function deleteApiKey(id: string, userId: number, database: Db = db): boolean {
|
|
const existing = database
|
|
.select()
|
|
.from(apiKeys)
|
|
.where(eq(apiKeys.id, id))
|
|
.get();
|
|
|
|
if (!existing || existing.userId !== userId) return false;
|
|
|
|
const result = database.delete(apiKeys).where(eq(apiKeys.id, id)).run();
|
|
return result.changes > 0;
|
|
}
|