import { randomBytes, createHash } from 'node:crypto'; import { prisma } from '$lib/db'; export type ApiKeyRecord = { id: string; keyPrefix: string; createdAt: Date; expiresAt: Date | null; revokedAt: Date | null; }; export type GeneratedKey = { plaintext: string; prefix: string; hash: string; }; export function generateApiKey(): GeneratedKey { const raw = randomBytes(32); const hex = raw.toString('hex'); const plaintext = `sk_${hex}`; const prefix = plaintext.slice(0, 11); const hash = createHash('sha256').update(plaintext).digest('hex'); return { plaintext, prefix, hash }; } export async function validateApiKey(key: string): Promise { const hash = createHash('sha256').update(key).digest('hex'); const record = await prisma.apiKey.findUnique({ where: { keyHash: hash } }); if (!record) return null; if (record.revokedAt && record.revokedAt <= new Date()) return null; if (record.expiresAt && record.expiresAt <= new Date()) return null; return record; } export async function rotateApiKey(emergency = false): Promise<{ plaintext: string; prefix: string; previousExpiresAt: Date | null; previousRevokedAt: Date | null; }> { const now = new Date(); const activeKeys = await prisma.apiKey.findMany({ where: { revokedAt: null, OR: [{ expiresAt: null }, { expiresAt: { gt: now } }] } }); let previousExpiresAt: Date | null = null; let previousRevokedAt: Date | null = null; if (emergency) { previousRevokedAt = now; for (const key of activeKeys) { await prisma.apiKey.update({ where: { id: key.id }, data: { revokedAt: now } }); } previousExpiresAt = null; } else { const graceEnd = new Date(now.getTime() + 48 * 60 * 60 * 1000); previousExpiresAt = graceEnd; for (const key of activeKeys) { await prisma.apiKey.update({ where: { id: key.id }, data: { expiresAt: graceEnd } }); } previousRevokedAt = null; } const { plaintext, prefix, hash } = generateApiKey(); await prisma.apiKey.create({ data: { keyPrefix: prefix, keyHash: hash } }); return { plaintext, prefix, previousExpiresAt, previousRevokedAt }; }