// server/supabase/functions/stt-proxy/index.ts // D3RO Voice — Multi-Provider Cloud Speech-to-Text Proxy // Routes through the self-hosted D3RO API gateway, then direct providers (Groq, OpenAI, Deepgram). // // This file is the composition root. The use case and fallback policy live in // handler.ts, the provider adapters in providers.ts, the wire contract in // ../_shared/stt-contract.ts. import { corsHeaders } from '../_shared/cors.ts' import { authErrorResponse, type AuthError, requireUser } from '../_shared/auth.ts' import { createServiceRoleClient, finalizeSttQuota, reserveSttQuota } from '../_shared/quota.ts' import { buildDictionaryHints } from '../_shared/stt-contract.ts' import { readProviderKey } from '../_shared/provider-key.ts' import { type AuthenticateResult, createSttProxyHandler, type SttDictionaryPort, type SttQuotaPort, } from './handler.ts' import { createSttProviders, type SttProvider } from './providers.ts' function isAuthError(err: unknown): err is AuthError { return !!err && typeof err === 'object' && 'status' in err && (err.status === 401 || err.status === 403) && 'message' in err && typeof err.message === 'string' } async function authenticate(req: Request): Promise { try { const user = await requireUser(req) return { ok: true, user: { id: user.id } } } catch (err) { if (isAuthError(err)) return { ok: false, response: authErrorResponse(err, corsHeaders) } throw err } } /** The service-role client is created per call so a missing env becomes a 500, not a boot failure. */ const supabaseSttQuota: SttQuotaPort = { reserve: (userId, reservationId) => reserveSttQuota(userId, reservationId, createServiceRoleClient()), finalize: (reservationId, succeeded) => finalizeSttQuota(reservationId, succeeded, createServiceRoleClient()), } const supabaseDictionary: SttDictionaryPort = { async hintsFor(userId) { const query = await createServiceRoleClient() .from('dictionary') .select('word,pronunciation') .eq('user_id', userId) .order('usage_count', { ascending: false }) .limit(50) return buildDictionaryHints(query.error ? [] : (query.data ?? [])) }, } const logger = { warn: (message: string, meta: Record) => console.warn(message, meta), error: (message: string, meta: Record) => console.error(message, meta), } function providersFromEnv(): SttProvider[] { return createSttProviders({ gatewayUrl: Deno.env.get('D3RO_API_URL') ?? Deno.env.get('BACKEND_ORIGIN') ?? '', // Supabase user JWTs are not valid D3RO API JWTs. Configure a dedicated backend token. gatewayToken: Deno.env.get('D3RO_API_TOKEN') ?? '', groqKey: readProviderKey('GROQ_API_KEY'), openaiKey: readProviderKey('OPENAI_API_KEY'), deepgramKey: readProviderKey('DEEPGRAM_API_KEY'), }, { logger }) } Deno.serve(createSttProxyHandler({ authenticate, quota: supabaseSttQuota, dictionary: supabaseDictionary, providers: providersFromEnv, logger, }))