import crypto from 'crypto' import { createClient } from '@supabase/supabase-js' const KEY_PREFIX = 'gnubok_sk_' /** * Create a Supabase service client that doesn't require cookies. * Used for API key validation (MCP, webhooks) where there's no browser session. */ export function createServiceClientNoCookies() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) } export function generateApiKey(): { key: string; hash: string; prefix: string } { const random = crypto.randomBytes(32).toString('base64url') const key = `${KEY_PREFIX}${random}` const hash = hashApiKey(key) const prefix = key.slice(0, KEY_PREFIX.length + 8) return { key, hash, prefix } } export function hashApiKey(key: string): string { return crypto.createHash('sha256').update(key).digest('hex') } export function extractBearerToken(request: Request): string | null { const authHeader = request.headers.get('authorization') if (!authHeader?.startsWith('Bearer ')) return null return authHeader.slice(7) } /** * Validate an API key and enforce rate limiting. * Uses the DB RPC for atomic check + increment. * Returns the user_id on success, or an error with HTTP status. */ export async function validateApiKey( key: string ): Promise<{ userId: string } | { error: string; status: number }> { if (!key.startsWith(KEY_PREFIX)) { return { error: 'Invalid API key format', status: 401 } } const hash = hashApiKey(key) const supabase = createServiceClientNoCookies() const { data, error } = await supabase.rpc('validate_and_increment_api_key', { p_key_hash: hash, }) if (error || !data || data.length === 0) { return { error: 'Invalid API key', status: 401 } } const row = data[0] if (row.rate_limited) { return { error: 'Rate limit exceeded', status: 429 } } return { userId: row.user_id } }